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