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