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 { severity, shape } => {
209 let shape = self.intern(shape);
210 self.cons(Shape::Annotated { severity, shape })
211 }
212 Shape::Top => self.top(),
213 Shape::Not(c) => {
214 let cn = self.intern(c);
215 self.mk_not(cn)
216 }
217 Shape::And(cs) => {
218 let ids = cs.iter().map(|c| self.intern(*c)).collect();
219 self.mk_and(ids)
220 }
221 Shape::Or(cs) => {
222 let ids = cs.iter().map(|c| self.intern(*c)).collect();
223 self.mk_or(ids)
224 }
225 Shape::Count {
226 path,
227 min,
228 max,
229 qualifier,
230 } => {
231 let q = self.intern(qualifier);
232 self.mk_count(normalize_path(path), min, max, q)
233 }
234 Shape::TestType(vt) => match vt.normalize() {
236 None => self.bottom(), Some(ValueType::Any) => self.top(), Some(v) => self.cons(Shape::TestType(v)),
239 },
240 Shape::Eq(path, nn) => self.cons(Shape::Eq(normalize_path(path), nn)),
242 Shape::Disj(path, nn) => self.cons(Shape::Disj(normalize_path(path), nn)),
243 Shape::Lt(path, nn) => self.cons(Shape::Lt(normalize_path(path), nn)),
244 Shape::Le(path, nn) => self.cons(Shape::Le(normalize_path(path), nn)),
245 Shape::UniqueLang(path) => self.cons(Shape::UniqueLang(normalize_path(path))),
246 leaf => self.cons(leaf),
248 }
249 }
250
251 fn rebuild_cyclic(&mut self, id: ShapeId) -> Shape {
254 match self.src.get(id).clone() {
255 Shape::Annotated { severity, shape } => Shape::Annotated {
256 severity,
257 shape: self.intern(shape),
258 },
259 Shape::Not(c) => Shape::Not(self.intern(c)),
260 Shape::And(cs) => Shape::And(self.intern_set(&cs)),
261 Shape::Or(cs) => Shape::Or(self.intern_set(&cs)),
262 Shape::Count {
263 path,
264 min,
265 max,
266 qualifier,
267 } => Shape::Count {
268 path: normalize_path(path),
269 min,
270 max,
271 qualifier: self.intern(qualifier),
272 },
273 leaf => leaf,
274 }
275 }
276
277 fn intern_set(&mut self, cs: &[ShapeId]) -> Vec<ShapeId> {
278 let mut v: Vec<ShapeId> = cs.iter().map(|c| self.intern(*c)).collect();
279 v.sort();
280 v.dedup();
281 v
282 }
283
284 fn mk_not(&mut self, c: ShapeId) -> ShapeId {
287 if let Shape::Not(x) = self.dst.get(c) {
288 return *x; }
290 if self.cyclic_dst.contains(&c) {
291 return self.cons(Shape::Not(c)); }
293 match self.dst.get(c).clone() {
294 Shape::And(cs) => {
296 let neg = cs.iter().map(|c| self.mk_not(*c)).collect();
297 self.mk_or(neg)
298 }
299 Shape::Or(cs) => {
300 let neg = cs.iter().map(|c| self.mk_not(*c)).collect();
301 self.mk_and(neg)
302 }
303 Shape::Count {
305 path,
306 min,
307 max,
308 qualifier,
309 } => {
310 let mut alts = Vec::new();
311 if let Some(a) = min
312 && a > 0
313 {
314 alts.push(self.mk_count(path.clone(), None, Some(a - 1), qualifier));
315 }
316 if let Some(b) = max {
317 alts.push(self.mk_count(path, Some(b + 1), None, qualifier));
318 }
319 self.mk_or(alts)
320 }
321 Shape::TestKind(k) => {
323 let comp: NodeKindSet = k.complement();
324 if comp.is_empty() {
325 self.bottom() } else {
327 self.cons(Shape::TestKind(comp))
328 }
329 }
330 _ => self.cons(Shape::Not(c)),
332 }
333 }
334
335 fn mk_and(&mut self, ids: Vec<ShapeId>) -> ShapeId {
336 let mut flat = Vec::new();
338 for id in ids {
339 match self.dst.get(id) {
340 Shape::And(inner) => flat.extend(inner.iter().copied()),
341 _ => flat.push(id),
342 }
343 }
344 let flat = self.merge_counts(flat);
346 let flat = self.merge_value_types(flat);
348 let flat = self.merge_node_kinds(flat);
350 let mut acc = Vec::new();
352 for id in flat {
353 if self.is_bottom(id) {
354 return id; }
356 if self.is_top(id) {
357 continue; }
359 acc.push(id);
360 }
361 acc.sort();
362 acc.dedup();
363 if self.has_complement(&acc) {
364 return self.bottom(); }
366 match acc.len() {
367 0 => self.top(),
368 1 => acc[0],
369 _ => self.cons(Shape::And(acc)),
370 }
371 }
372
373 fn merge_node_kinds(&mut self, flat: Vec<ShapeId>) -> Vec<ShapeId> {
377 let mut acc: Option<NodeKindSet> = None;
378 let mut others = Vec::new();
379 for id in flat {
380 match self.dst.get(id) {
381 Shape::TestKind(k) => {
382 acc = Some(match acc {
383 None => *k,
384 Some(prev) => NodeKindSet {
385 iri: prev.iri && k.iri,
386 blank: prev.blank && k.blank,
387 literal: prev.literal && k.literal,
388 },
389 });
390 }
391 _ => others.push(id),
392 }
393 }
394 if let Some(k) = acc {
395 if k.is_empty() {
396 others.push(self.bottom()); } else if k.iri && k.blank && k.literal {
398 } else {
400 let id = self.cons(Shape::TestKind(k));
401 others.push(id);
402 }
403 }
404 others
405 }
406
407 fn merge_counts(&mut self, flat: Vec<ShapeId>) -> Vec<ShapeId> {
411 let mut keys: Vec<(Path, ShapeId)> = Vec::new();
412 let mut bounds: Vec<(Option<u64>, Option<u64>)> = Vec::new();
413 let mut index: HashMap<(Path, ShapeId), usize> = HashMap::new();
414 let mut others = Vec::new();
415
416 for id in flat {
417 if let Shape::Count {
418 path,
419 min,
420 max,
421 qualifier,
422 } = self.dst.get(id).clone()
423 {
424 let key = (path, qualifier);
425 match index.get(&key) {
426 Some(&i) => {
427 bounds[i].0 = tighter_lower(bounds[i].0, min);
428 bounds[i].1 = tighter_upper(bounds[i].1, max);
429 }
430 None => {
431 index.insert(key.clone(), keys.len());
432 keys.push(key);
433 bounds.push((min, max));
434 }
435 }
436 } else {
437 others.push(id);
438 }
439 }
440
441 let mut result = others;
442 for ((path, q), (min, max)) in keys.into_iter().zip(bounds) {
443 let merged = self.mk_count(path, min, max, q);
444 result.push(merged);
445 }
446 result
447 }
448
449 fn merge_value_types(&mut self, flat: Vec<ShapeId>) -> Vec<ShapeId> {
454 let mut facets: Vec<ValueType> = Vec::new();
455 let mut others = Vec::new();
456 for id in flat {
457 match self.dst.get(id) {
458 Shape::TestType(vt) => facets.push(vt.clone()),
459 _ => others.push(id),
460 }
461 }
462 if facets.is_empty() {
463 return others;
464 }
465 match ValueType::and(facets).normalize() {
466 None => others.push(self.bottom()), Some(ValueType::Any) => {} Some(v) => {
469 let id = self.cons(Shape::TestType(v));
470 others.push(id);
471 }
472 }
473 others
474 }
475
476 fn mk_or(&mut self, ids: Vec<ShapeId>) -> ShapeId {
477 let mut flat = Vec::new();
478 for id in ids {
479 if self.is_top(id) {
480 return id; }
482 if self.is_bottom(id) {
483 continue; }
485 match self.dst.get(id) {
486 Shape::Or(inner) => flat.extend(inner.iter().copied()),
487 _ => flat.push(id),
488 }
489 }
490 flat.sort();
491 flat.dedup();
492 if self.has_complement(&flat) {
493 return self.top(); }
495 match flat.len() {
496 0 => self.bottom(),
497 1 => flat[0],
498 _ => self.cons(Shape::Or(flat)),
499 }
500 }
501
502 fn has_complement(&self, ids: &[ShapeId]) -> bool {
504 let set: HashSet<ShapeId> = ids.iter().copied().collect();
505 ids.iter().any(|&id| match self.dst.get(id) {
506 Shape::Not(x) => set.contains(x),
507 _ => false,
508 })
509 }
510
511 fn mk_count(
512 &mut self,
513 path: shifty_algebra::Path,
514 min: Option<u64>,
515 max: Option<u64>,
516 q: ShapeId,
517 ) -> ShapeId {
518 if max.is_none() && matches!(min, None | Some(0)) {
519 return self.top(); }
521 if let (Some(a), Some(b)) = (min, max)
522 && a > b
523 {
524 return self.bottom(); }
526 if matches!(&path, Path::Alt(v) if v.is_empty()) {
528 return if min.unwrap_or(0) >= 1 {
529 self.bottom() } else {
531 self.top() };
533 }
534 if self.is_bottom(q) {
536 return if min.unwrap_or(0) >= 1 {
537 self.bottom() } else {
539 self.top() };
541 }
542 if path == Path::Id {
544 let lo = min.unwrap_or(0);
545 if lo >= 2 {
546 return self.bottom(); }
548 return match (lo, max) {
549 (0, Some(0)) => self.mk_not(q), (1, _) => q, _ => self.top(), };
553 }
554 self.cons(Shape::Count {
555 path,
556 min,
557 max,
558 qualifier: q,
559 })
560 }
561
562 fn selector(&mut self, sel: &Selector) -> Selector {
563 match sel {
564 Selector::HasPath(p, q) => {
565 let path = normalize_path(p.clone());
566 let shape = self.intern(*q);
567 if self.is_top(shape) {
570 match &path {
571 Path::Pred(nn) => return Selector::HasOut(nn.clone()),
572 Path::Inverse(inner) => {
573 if let Path::Pred(nn) = inner.as_ref() {
574 return Selector::HasIn(nn.clone());
575 }
576 }
577 _ => {}
578 }
579 }
580 Selector::HasPath(path, shape)
581 }
582 other => other.clone(),
583 }
584 }
585
586 fn rule(&mut self, r: &Rule) -> Rule {
587 Rule {
588 selector: self.selector(&r.selector),
589 conditions: r.conditions.iter().map(|c| self.intern(*c)).collect(),
590 head: self.head(&r.head),
591 order: r.order,
592 deactivated: r.deactivated,
593 }
594 }
595
596 fn head(&mut self, h: &RuleHead) -> RuleHead {
597 match h {
598 RuleHead::Triple {
599 subject,
600 predicate,
601 object,
602 } => RuleHead::Triple {
603 subject: self.node_expr(subject),
604 predicate: self.node_expr(predicate),
605 object: self.node_expr(object),
606 },
607 RuleHead::Sparql(s) => RuleHead::Sparql(s.clone()),
608 }
609 }
610
611 fn node_expr(&mut self, e: &NodeExpr) -> NodeExpr {
612 match e {
613 NodeExpr::Filter { input, shape } => NodeExpr::Filter {
614 input: Box::new(self.node_expr(input)),
615 shape: self.intern(*shape),
616 },
617 NodeExpr::Intersection(v) => {
618 NodeExpr::Intersection(v.iter().map(|x| self.node_expr(x)).collect())
619 }
620 NodeExpr::Union(v) => NodeExpr::Union(v.iter().map(|x| self.node_expr(x)).collect()),
621 NodeExpr::Function { iri, args } => NodeExpr::Function {
622 iri: iri.clone(),
623 args: args.iter().map(|x| self.node_expr(x)).collect(),
624 },
625 other => other.clone(),
626 }
627 }
628}
629
630#[cfg(test)]
631mod tests {
632 use super::*;
633 use shifty_algebra::{NodeKindSet, Path, Selector};
634
635 fn schema_with(arena: ShapeArena, root: ShapeId) -> Schema {
636 Schema {
637 arena,
638 statements: vec![Statement {
639 selector: Selector::IsConst(shifty_algebra::Term::NamedNode(
640 shifty_algebra::NamedNode::new("http://ex/x").unwrap(),
641 )),
642 shape: root,
643 }],
644 rules: Vec::new(),
645 names: Default::default(),
646 }
647 }
648
649 #[test]
650 fn cse_dedups_identical_subshapes() {
651 let mut a = ShapeArena::new();
652 let t1 = a.insert(Shape::TestKind(NodeKindSet::IRI));
653 let t2 = a.insert(Shape::TestKind(NodeKindSet::IRI)); let root = a.insert(Shape::And(vec![t1, t2]));
655 let n = normalize(&schema_with(a, root));
656 let rooted = n.statements[0].shape;
658 assert!(matches!(n.arena.get(rooted), Shape::TestKind(_)));
659 assert_eq!(n.arena.len(), 1);
661 }
662
663 #[test]
664 fn bottom_absorbs_conjunction() {
665 let mut a = ShapeArena::new();
666 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
667 let t = a.insert(Shape::Top);
668 let bot = a.insert(Shape::Not(t));
669 let root = a.insert(Shape::And(vec![k, bot]));
670 let n = normalize(&schema_with(a, root));
671 let rooted = n.statements[0].shape;
672 assert!(
673 matches!(n.arena.get(rooted), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top))
674 );
675 }
676
677 #[test]
678 fn top_absorbs_disjunction() {
679 let mut a = ShapeArena::new();
680 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
681 let t = a.insert(Shape::Top);
682 let root = a.insert(Shape::Or(vec![k, t]));
683 let n = normalize(&schema_with(a, root));
684 assert!(matches!(n.arena.get(n.statements[0].shape), Shape::Top));
685 }
686
687 #[test]
688 fn complement_is_unsat() {
689 let mut a = ShapeArena::new();
690 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
691 let nk = a.insert(Shape::Not(k));
692 let root = a.insert(Shape::And(vec![k, nk]));
693 let n = normalize(&schema_with(a, root));
694 assert!(
695 matches!(n.arena.get(n.statements[0].shape), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top))
696 );
697 }
698
699 #[test]
700 fn nnf_pushes_negation_through_and() {
701 let mut a = ShapeArena::new();
704 let p = Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap());
705 let top = a.insert(Shape::Top);
706 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
707 let cnt = a.insert(Shape::Count {
708 path: p,
709 min: Some(1),
710 max: None,
711 qualifier: top,
712 });
713 let and = a.insert(Shape::And(vec![k, cnt]));
714 let root = a.insert(Shape::Not(and));
715 let n = normalize(&schema_with(a, root));
716 match n.arena.get(n.statements[0].shape) {
717 Shape::Or(cs) => {
718 assert_eq!(cs.len(), 2);
719 let kinds: Vec<_> = cs
720 .iter()
721 .map(|c| n.arena.get(*c))
722 .map(|s| matches!(s, Shape::TestKind(_) | Shape::Count { .. }))
723 .collect();
724 assert!(
725 kinds.iter().all(|&b| b),
726 "expected Or of TestKind+Count, got something else"
727 );
728 }
729 other => panic!("expected Or of two shapes, got {other:?}"),
730 }
731 }
732
733 #[test]
734 fn disjoint_node_kinds_in_and_is_unsat() {
735 let mut a = ShapeArena::new();
737 let iri = a.insert(Shape::TestKind(NodeKindSet::IRI));
738 let lit = a.insert(Shape::TestKind(NodeKindSet::LITERAL));
739 let root = a.insert(Shape::And(vec![iri, lit]));
740 let n = normalize(&schema_with(a, root));
741 assert!(
742 matches!(n.arena.get(n.statements[0].shape), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top))
743 );
744 }
745
746 #[test]
747 fn nnf_flips_count_bound() {
748 let mut a = ShapeArena::new();
750 let top = a.insert(Shape::Top);
751 let count = a.insert(Shape::Count {
752 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
753 min: Some(2),
754 max: None,
755 qualifier: top,
756 });
757 let root = a.insert(Shape::Not(count));
758 let n = normalize(&schema_with(a, root));
759 match n.arena.get(n.statements[0].shape) {
760 Shape::Count { min, max, .. } => {
761 assert_eq!((*min, *max), (None, Some(1)));
762 }
763 other => panic!("expected ∃≤1, got {other:?}"),
764 }
765 }
766
767 #[test]
768 fn merges_min_and_max_counts() {
769 let mut a = ShapeArena::new();
771 let p = Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap());
772 let t1 = a.insert(Shape::Top);
773 let t2 = a.insert(Shape::Top);
774 let lo = a.insert(Shape::Count {
775 path: p.clone(),
776 min: Some(1),
777 max: None,
778 qualifier: t1,
779 });
780 let hi = a.insert(Shape::Count {
781 path: p,
782 min: None,
783 max: Some(1),
784 qualifier: t2,
785 });
786 let root = a.insert(Shape::And(vec![lo, hi]));
787 let n = normalize(&schema_with(a, root));
788 match n.arena.get(n.statements[0].shape) {
789 Shape::Count { min, max, .. } => assert_eq!((*min, *max), (Some(1), Some(1))),
790 other => panic!("expected one fused ∃[1..1], got {other:?}"),
791 }
792 }
793
794 #[test]
795 fn merged_counts_can_be_unsat() {
796 let mut a = ShapeArena::new();
798 let p = Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap());
799 let t1 = a.insert(Shape::Top);
800 let t2 = a.insert(Shape::Top);
801 let lo = a.insert(Shape::Count {
802 path: p.clone(),
803 min: Some(2),
804 max: None,
805 qualifier: t1,
806 });
807 let hi = a.insert(Shape::Count {
808 path: p,
809 min: None,
810 max: Some(1),
811 qualifier: t2,
812 });
813 let root = a.insert(Shape::And(vec![lo, hi]));
814 let n = normalize(&schema_with(a, root));
815 assert!(
816 matches!(n.arena.get(n.statements[0].shape), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top))
817 );
818 }
819
820 #[test]
821 fn unsat_value_type_absorbs_conjunction() {
822 use shifty_algebra::{Bound, Literal, NamedNode, ValueType};
824 let int = |n: i64| {
825 Literal::new_typed_literal(
826 n.to_string(),
827 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
828 )
829 };
830 let mut a = ShapeArena::new();
831 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
832 let bad = a.insert(Shape::TestType(ValueType::NumericRange {
833 lo: Some(Bound {
834 value: int(5),
835 inclusive: true,
836 }),
837 hi: Some(Bound {
838 value: int(3),
839 inclusive: true,
840 }),
841 }));
842 let root = a.insert(Shape::And(vec![k, bad]));
843 let n = normalize(&schema_with(a, root));
844 assert!(matches!(n.arena.get(n.statements[0].shape),
845 Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top)));
846 }
847
848 #[test]
849 fn conjoined_range_facets_merge() {
850 use shifty_algebra::{Bound, Literal, NamedNode, ValueType};
852 let int = |n: i64| {
853 Literal::new_typed_literal(
854 n.to_string(),
855 NamedNode::new("http://www.w3.org/2001/XMLSchema#integer").unwrap(),
856 )
857 };
858 let mut a = ShapeArena::new();
859 let lo = a.insert(Shape::TestType(ValueType::NumericRange {
860 lo: Some(Bound {
861 value: int(1),
862 inclusive: true,
863 }),
864 hi: None,
865 }));
866 let hi = a.insert(Shape::TestType(ValueType::NumericRange {
867 lo: None,
868 hi: Some(Bound {
869 value: int(10),
870 inclusive: true,
871 }),
872 }));
873 let root = a.insert(Shape::And(vec![lo, hi]));
874 let n = normalize(&schema_with(a, root));
875 match n.arena.get(n.statements[0].shape) {
876 Shape::TestType(ValueType::NumericRange { lo, hi }) => {
877 assert!(lo.is_some() && hi.is_some(), "expected fused [1,10]");
878 }
879 other => panic!("expected one fused range facet, got {other:?}"),
880 }
881 }
882
883 #[test]
884 fn negating_recursive_shape_terminates() {
885 let mut a = ShapeArena::new();
887 let s = a.reserve();
888 a.set(
889 s,
890 Shape::Count {
891 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
892 min: Some(1),
893 max: None,
894 qualifier: s,
895 },
896 );
897 let root = a.insert(Shape::Not(s));
898 let n = normalize(&schema_with(a, root));
899 assert!(matches!(n.arena.get(n.statements[0].shape), Shape::Not(_)));
900 }
901
902 #[test]
903 fn qualifier_bottom_min1_is_bottom() {
904 let mut a = ShapeArena::new();
906 let top = a.insert(Shape::Top);
907 let bot = a.insert(Shape::Not(top));
908 let count = a.insert(Shape::Count {
909 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
910 min: Some(1),
911 max: None,
912 qualifier: bot,
913 });
914 let n = normalize(&schema_with(a, count));
915 let r = n.statements[0].shape;
916 assert!(matches!(n.arena.get(r), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top)));
917 }
918
919 #[test]
920 fn qualifier_bottom_max_bound_is_top() {
921 let mut a = ShapeArena::new();
923 let top = a.insert(Shape::Top);
924 let bot = a.insert(Shape::Not(top));
925 let count = a.insert(Shape::Count {
926 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
927 min: None,
928 max: Some(2),
929 qualifier: bot,
930 });
931 let n = normalize(&schema_with(a, count));
932 assert!(matches!(n.arena.get(n.statements[0].shape), Shape::Top));
933 }
934
935 #[test]
936 fn id_path_min1_is_qualifier() {
937 let mut a = ShapeArena::new();
939 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
940 let count = a.insert(Shape::Count {
941 path: Path::Id,
942 min: Some(1),
943 max: None,
944 qualifier: k,
945 });
946 let n = normalize(&schema_with(a, count));
947 assert!(matches!(
948 n.arena.get(n.statements[0].shape),
949 Shape::TestKind(NodeKindSet::IRI)
950 ));
951 }
952
953 #[test]
954 fn id_path_max0_is_negation() {
955 let mut a = ShapeArena::new();
957 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
958 let count = a.insert(Shape::Count {
959 path: Path::Id,
960 min: None,
961 max: Some(0),
962 qualifier: k,
963 });
964 let n = normalize(&schema_with(a, count));
965 match n.arena.get(n.statements[0].shape) {
966 Shape::TestKind(nk) => {
967 assert_eq!(*nk, NodeKindSet::BLANK_NODE_OR_LITERAL);
968 }
969 other => panic!("expected TestKind(Blank|Lit), got {other:?}"),
970 }
971 }
972
973 #[test]
974 fn id_path_min2_is_bottom() {
975 let mut a = ShapeArena::new();
977 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
978 let count = a.insert(Shape::Count {
979 path: Path::Id,
980 min: Some(2),
981 max: None,
982 qualifier: k,
983 });
984 let n = normalize(&schema_with(a, count));
985 let r = n.statements[0].shape;
986 assert!(matches!(n.arena.get(r), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top)));
987 }
988
989 #[test]
990 fn converse_pushdown_through_seq() {
991 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
993 let q = shifty_algebra::NamedNode::new("http://ex/q").unwrap();
994 let inv_seq = Path::Inverse(Box::new(Path::Seq(vec![
995 Path::Pred(p.clone()),
996 Path::Pred(q.clone()),
997 ])));
998 let mut a = ShapeArena::new();
999 let top = a.insert(Shape::Top);
1000 let count = a.insert(Shape::Count {
1001 path: inv_seq,
1002 min: Some(1),
1003 max: None,
1004 qualifier: top,
1005 });
1006 let n = normalize(&schema_with(a, count));
1007 match n.arena.get(n.statements[0].shape) {
1008 Shape::Count { path, .. } => {
1009 let expected = Path::Seq(vec![
1010 Path::Inverse(Box::new(Path::Pred(q))),
1011 Path::Inverse(Box::new(Path::Pred(p))),
1012 ]);
1013 assert_eq!(*path, expected, "expected (p·q)⁻ normalized to q⁻·p⁻");
1014 }
1015 other => panic!("expected Count with normalized path, got {other:?}"),
1016 }
1017 }
1018
1019 #[test]
1020 fn converse_pushdown_through_alt_and_star() {
1021 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
1023 let q = shifty_algebra::NamedNode::new("http://ex/q").unwrap();
1024 let alt = Path::Alt(vec![Path::Pred(p.clone()), Path::Pred(q.clone())]);
1025 assert_eq!(
1026 normalize_path(Path::Inverse(Box::new(alt))),
1027 Path::Alt(vec![
1028 Path::Inverse(Box::new(Path::Pred(p.clone()))),
1029 Path::Inverse(Box::new(Path::Pred(q))),
1030 ])
1031 );
1032 let star_inv = Path::Inverse(Box::new(Path::Star(Box::new(Path::Pred(p.clone())))));
1033 assert_eq!(
1034 normalize_path(star_inv),
1035 Path::Star(Box::new(Path::Inverse(Box::new(Path::Pred(p)))))
1036 );
1037 }
1038
1039 #[test]
1040 fn recursive_shape_survives_normalization() {
1041 let mut a = ShapeArena::new();
1043 let s = a.reserve();
1044 a.set(
1045 s,
1046 Shape::Count {
1047 path: Path::Pred(shifty_algebra::NamedNode::new("http://ex/p").unwrap()),
1048 min: Some(1),
1049 max: None,
1050 qualifier: s,
1051 },
1052 );
1053 let n = normalize(&schema_with(a, s));
1054 let rooted = n.statements[0].shape;
1055 match n.arena.get(rooted) {
1056 Shape::Count { qualifier, .. } => assert_eq!(*qualifier, rooted),
1057 other => panic!("expected self-referential Count, got {other:?}"),
1058 }
1059 }
1060
1061 #[test]
1062 fn negkind_iri_becomes_blank_or_literal() {
1063 let mut a = ShapeArena::new();
1065 let iri = a.insert(Shape::TestKind(NodeKindSet::IRI));
1066 let root = a.insert(Shape::Not(iri));
1067 let n = normalize(&schema_with(a, root));
1068 assert!(matches!(
1069 n.arena.get(n.statements[0].shape),
1070 Shape::TestKind(NodeKindSet::BLANK_NODE_OR_LITERAL)
1071 ));
1072 }
1073
1074 #[test]
1075 fn empty_alt_path_min1_is_bottom() {
1076 let mut a = ShapeArena::new();
1078 let top = a.insert(Shape::Top);
1079 let count = a.insert(Shape::Count {
1080 path: Path::Alt(vec![]),
1081 min: Some(1),
1082 max: None,
1083 qualifier: top,
1084 });
1085 let n = normalize(&schema_with(a, count));
1086 let r = n.statements[0].shape;
1087 assert!(matches!(n.arena.get(r), Shape::Not(x) if matches!(n.arena.get(*x), Shape::Top)));
1088 }
1089
1090 #[test]
1091 fn empty_alt_path_max_bound_is_top() {
1092 let mut a = ShapeArena::new();
1094 let top = a.insert(Shape::Top);
1095 let count = a.insert(Shape::Count {
1096 path: Path::Alt(vec![]),
1097 min: None,
1098 max: Some(3),
1099 qualifier: top,
1100 });
1101 let n = normalize(&schema_with(a, count));
1102 assert!(matches!(n.arena.get(n.statements[0].shape), Shape::Top));
1103 }
1104
1105 #[test]
1106 fn star_drops_id_from_alt() {
1107 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
1109 let alt_id = Path::Alt(vec![Path::Pred(p.clone()), Path::Id]);
1110 let star = Path::Star(Box::new(alt_id));
1111 assert_eq!(normalize_path(star), Path::Star(Box::new(Path::Pred(p))));
1112 }
1113
1114 #[test]
1115 fn seq_merges_adjacent_equal_stars() {
1116 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
1118 let star = Path::Star(Box::new(Path::Pred(p.clone())));
1119 let seq = Path::Seq(vec![star.clone(), star]);
1120 assert_eq!(normalize_path(seq), Path::Star(Box::new(Path::Pred(p))));
1121 }
1122
1123 #[test]
1124 fn alt_deduplicates_paths() {
1125 let p = shifty_algebra::NamedNode::new("http://ex/p").unwrap();
1127 let q = shifty_algebra::NamedNode::new("http://ex/q").unwrap();
1128 let dup = Path::Alt(vec![
1129 Path::Pred(p.clone()),
1130 Path::Pred(p.clone()),
1131 Path::Pred(q.clone()),
1132 ]);
1133 let result = normalize_path(dup);
1134 assert_eq!(result, Path::Alt(vec![Path::Pred(p), Path::Pred(q)]));
1135 }
1136
1137 #[test]
1138 fn selector_haspath_pred_top_becomes_hasout() {
1139 let q = shifty_algebra::NamedNode::new("http://ex/q").unwrap();
1141 let mut a = ShapeArena::new();
1142 let top = a.insert(Shape::Top);
1143 let schema = Schema {
1144 arena: a,
1145 statements: vec![Statement {
1146 selector: Selector::HasPath(Path::Pred(q.clone()), top),
1147 shape: top,
1148 }],
1149 rules: vec![],
1150 names: Default::default(),
1151 };
1152 let n = normalize(&schema);
1153 assert_eq!(n.statements[0].selector, Selector::HasOut(q));
1154 }
1155
1156 #[test]
1157 fn selector_haspath_inv_pred_top_becomes_hasin() {
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::Inverse(Box::new(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::HasIn(q));
1173 }
1174
1175 #[test]
1176 fn statement_dedup_removes_identical() {
1177 let mut a = ShapeArena::new();
1178 let k = a.insert(Shape::TestKind(NodeKindSet::IRI));
1179 let node =
1180 shifty_algebra::Term::NamedNode(shifty_algebra::NamedNode::new("http://ex/x").unwrap());
1181 let sel = Selector::IsConst(node);
1182 let schema = Schema {
1183 arena: a,
1184 statements: vec![
1185 Statement {
1186 selector: sel.clone(),
1187 shape: k,
1188 },
1189 Statement {
1190 selector: sel.clone(),
1191 shape: k,
1192 },
1193 ],
1194 rules: vec![],
1195 names: Default::default(),
1196 };
1197 let n = normalize(&schema);
1198 assert_eq!(n.statements.len(), 1);
1199 }
1200}