Skip to main content

shifty_parse/
lower.rs

1//! Lower a loaded shapes graph into the formalism [`Schema`].
2//!
3//! Every SHACL Core construct collapses into the small IR, applying the sugar
4//! rules from the gap analysis (`class → path`, `minCount/maxCount → Count`,
5//! per-value constraints wrapped in `∀π = ∃≤0 π.¬φ`, `xone → ∧∨¬`, …). Each
6//! shape lowers to a **focus-node predicate** `φ`, so `sh:property`/`sh:node`
7//! compose by conjunction. Unsupported AF constructs emit diagnostics.
8
9use crate::diagnostics::{DiagLevel, Diagnostic};
10use crate::graph::{Loaded, term_to_node};
11use crate::path::parse_path;
12use crate::vocab;
13use oxrdf::{Literal, NamedNode, NamedOrBlankNode, Term};
14use shifty_algebra::{
15    Bound, NodeExpr, NodeKindSet, Path, Rule, RuleHead, Schema, Selector, Severity, Shape,
16    ShapeArena, ShapeId, SparqlConstraint, SparqlConstruct, SparqlQueryKind, SparqlTarget,
17    Statement, ValueType,
18};
19use spargebra::{Query, SparqlParser};
20use std::collections::{BTreeSet, HashMap, HashSet};
21
22pub struct Lowered {
23    pub schema: Schema,
24    pub diagnostics: Vec<Diagnostic>,
25}
26
27/// Lower a loaded graph into a schema plus diagnostics.
28pub fn lower(g: &Loaded) -> Lowered {
29    let mut l = Lowerer {
30        g,
31        arena: ShapeArena::new(),
32        cache: HashMap::new(),
33        statements: Vec::new(),
34        rules: Vec::new(),
35        diags: Vec::new(),
36    };
37    let shapes = l.discover_shapes();
38    for s in &shapes {
39        l.lower_shape(s);
40    }
41    l.lower_custom_components(&shapes);
42    for s in &shapes {
43        // selectors are shared by the shape's statements and its rules
44        let selectors = l.target_selectors(s);
45        if let Some(shape) = l.cache.get(s).copied() {
46            for sel in &selectors {
47                l.statements.push(Statement {
48                    selector: sel.clone(),
49                    shape,
50                });
51            }
52        }
53        l.parse_rules(s, &selectors);
54    }
55    let names = l
56        .cache
57        .iter()
58        .filter_map(|(node, id)| match node {
59            NamedOrBlankNode::NamedNode(n) => Some((*id, n.as_str().to_string())),
60            NamedOrBlankNode::BlankNode(_) => None,
61        })
62        .collect();
63    let schema = Schema {
64        arena: l.arena,
65        statements: l.statements,
66        rules: l.rules,
67        names,
68    };
69    schema.arena.debug_assert_finalized();
70    Lowered {
71        schema,
72        diagnostics: l.diags,
73    }
74}
75
76struct Lowerer<'a> {
77    g: &'a Loaded,
78    arena: ShapeArena,
79    cache: HashMap<NamedOrBlankNode, ShapeId>,
80    statements: Vec<Statement>,
81    rules: Vec<Rule>,
82    diags: Vec<Diagnostic>,
83}
84
85struct ComponentDef {
86    iri: NamedNode,
87    params: Vec<ParamDef>,
88    node_validator: Option<ValidatorDef>,
89    property_validator: Option<ValidatorDef>,
90    generic_validator: Option<ValidatorDef>,
91}
92
93struct ParamDef {
94    path: NamedNode,
95    var: String,
96    optional: bool,
97}
98
99struct ValidatorDef {
100    kind: SparqlQueryKind,
101    query: String,
102    messages: Vec<Term>,
103}
104
105/// The local name of an IRI (after the last `#` or `/`) — the SHACL rule for a
106/// parameter's pre-bound variable name (SHACL §6.2.1).
107fn local_name(iri: &str) -> &str {
108    iri.rsplit(['#', '/']).next().unwrap_or(iri)
109}
110
111impl Lowerer<'_> {
112    fn diag(&mut self, level: DiagLevel, msg: impl Into<String>, subj: &NamedOrBlankNode) {
113        self.diags
114            .push(Diagnostic::new(level, msg, Some(subj.to_string())));
115    }
116
117    /// Subjects that are declared shapes: typed NodeShape/PropertyShape, or
118    /// carrying `sh:path` or a target predicate. Referenced-only shapes are
119    /// pulled in on demand during lowering. Sorted for deterministic output.
120    fn discover_shapes(&self) -> Vec<NamedOrBlankNode> {
121        let mut found: HashSet<NamedOrBlankNode> = HashSet::new();
122        for triple in self.g.graph.iter() {
123            let p = triple.predicate;
124            let is_target = p == vocab::SH_TARGET_NODE
125                || p == vocab::SH_TARGET_CLASS
126                || p == vocab::SH_TARGET_SUBJECTS_OF
127                || p == vocab::SH_TARGET_OBJECTS_OF
128                || p == vocab::SH_TARGET;
129            if p == vocab::SH_PATH || p == vocab::SH_SPARQL || p == vocab::SH_RULE || is_target {
130                found.insert(triple.subject.into_owned());
131            }
132            if p == vocab::RDF_TYPE
133                && let Term::NamedNode(ty) = triple.object.into_owned()
134                && (ty.as_ref() == vocab::SH_NODE_SHAPE || ty.as_ref() == vocab::SH_PROPERTY_SHAPE)
135            {
136                found.insert(triple.subject.into_owned());
137            }
138        }
139        let mut shapes: Vec<NamedOrBlankNode> = found.into_iter().collect();
140        shapes.sort_by_key(|n| n.to_string());
141        shapes
142    }
143
144    fn lower_shape(&mut self, s: &NamedOrBlankNode) -> ShapeId {
145        if let Some(id) = self.cache.get(s) {
146            return *id;
147        }
148        let id = self.arena.reserve();
149        self.cache.insert(s.clone(), id);
150
151        if self.bool_prop(s, vocab::SH_DEACTIVATED) {
152            self.arena.set(id, Shape::Top);
153            return id;
154        }
155
156        let path = self.parse_shape_path(s);
157        let mut conjuncts: Vec<ShapeId> = Vec::new();
158
159        // Value-scoped constraints: each applies to every value node along the
160        // path (or to the focus node directly when there is no path).
161        let value = self.collect_value_constraints(s);
162        if !value.is_empty() {
163            let value_phi = self.arena.and(value);
164            match &path {
165                Some(p) => {
166                    // ∀π.φ  ≡  ∃≤0 π.¬φ
167                    let neg = self.arena.not(value_phi);
168                    let c = self.arena.count(p.clone(), None, Some(0), neg);
169                    conjuncts.push(c);
170                }
171                None => conjuncts.push(value_phi),
172            }
173        }
174
175        self.collect_path_constraints(s, path.as_ref(), &mut conjuncts);
176
177        if self.bool_prop(s, vocab::SH_CLOSED) {
178            let q = self.closed_allowed(s);
179            let c = self.arena.insert(Shape::Closed(q));
180            conjuncts.push(c);
181        }
182
183        for constraint_term in self.g.objects(s, vocab::SH_SPARQL) {
184            let Some(constraint_node) = term_to_node(&constraint_term) else {
185                self.diag(DiagLevel::Error, "sh:sparql must reference a resource", s);
186                continue;
187            };
188            let parsed = if let Some(Term::Literal(query)) =
189                self.g.object(&constraint_node, vocab::SH_SELECT)
190            {
191                self.canonical_sparql(&constraint_node, query.value(), ExpectedQuery::Select)
192                    .map(|query| (SparqlQueryKind::Select, query))
193            } else if let Some(Term::Literal(query)) =
194                self.g.object(&constraint_node, vocab::SH_ASK)
195            {
196                self.canonical_sparql(&constraint_node, query.value(), ExpectedQuery::Ask)
197                    .map(|query| (SparqlQueryKind::Ask, query))
198            } else {
199                self.diag(
200                    DiagLevel::Error,
201                    "sh:sparql constraint requires sh:select or sh:ask",
202                    &constraint_node,
203                );
204                None
205            };
206            if let Some((kind, query)) = parsed {
207                let shape = Some(match s {
208                    NamedOrBlankNode::NamedNode(n) => Term::NamedNode(n.clone()),
209                    NamedOrBlankNode::BlankNode(b) => Term::BlankNode(b.clone()),
210                });
211                // `sh:message` on the SPARQL constraint takes precedence; absent
212                // that, fall back to the owning shape's `sh:message` (SHACL §5.2.1).
213                let mut messages: Vec<Term> = self.g.objects(&constraint_node, vocab::SH_MESSAGE);
214                if messages.is_empty() {
215                    messages = self.g.objects(s, vocab::SH_MESSAGE);
216                }
217                let constraint = SparqlConstraint {
218                    kind,
219                    query,
220                    path: path.clone(),
221                    shape,
222                    messages,
223                    extra_bindings: Vec::new(),
224                    bind_value_to_this: false,
225                };
226                conjuncts.push(self.arena.insert(Shape::Sparql(constraint)));
227            }
228        }
229
230        // sh:expression (SHACL-AF §5): the node expression must evaluate to
231        // `true` with the focus node as `?this`. Focus-scoped, so it joins the
232        // conjuncts directly rather than under a `∀π` wrapper.
233        for expr_term in self.g.objects(s, vocab::SH_EXPRESSION) {
234            if let Some(expr) = self.parse_node_expr(expr_term, s) {
235                if node_expr_has_function(&expr) {
236                    // SPARQL functions inside expressions need shapes-graph
237                    // lookups the validation evaluators don't perform; refuse
238                    // rather than silently under-constrain.
239                    self.diag(
240                        DiagLevel::Unsupported,
241                        "sh:expression with a function call is not yet evaluated",
242                        s,
243                    );
244                } else {
245                    conjuncts.push(self.arena.insert(Shape::Expression(expr)));
246                }
247            }
248        }
249
250        let body = if conjuncts.is_empty() {
251            self.arena.top()
252        } else if conjuncts.len() == 1 {
253            if conjuncts[0] == id {
254                self.arena.top()
255            } else {
256                conjuncts[0]
257            }
258        } else {
259            self.arena.insert(Shape::And(conjuncts))
260        };
261        self.arena.set(
262            id,
263            Shape::Annotated {
264                severity: self.severity(s),
265                shape: body,
266            },
267        );
268        id
269    }
270
271    fn severity(&self, shape: &NamedOrBlankNode) -> Severity {
272        match self.g.object(shape, vocab::SH_SEVERITY) {
273            Some(Term::NamedNode(value)) => Severity::from_named_node(value),
274            _ => Severity::Violation,
275        }
276    }
277
278    fn collect_value_constraints(&mut self, s: &NamedOrBlankNode) -> Vec<ShapeId> {
279        let mut value: Vec<ShapeId> = Vec::new();
280
281        // sh:class C  ≡  ∃≥1 (rdf:type/rdfs:subClassOf*) . test(C)
282        for c in self.g.objects(s, vocab::SH_CLASS) {
283            let tn = self.arena.insert(Shape::TestConst(c));
284            let cc = self.arena.count(class_path(), Some(1), None, tn);
285            value.push(cc);
286        }
287
288        // sh:datatype
289        for d in self.g.objects(s, vocab::SH_DATATYPE) {
290            if let Term::NamedNode(n) = d {
291                let id = self.arena.insert(Shape::TestType(ValueType::Datatype(n)));
292                value.push(id);
293            }
294        }
295
296        // sh:nodeKind
297        for k in self.g.objects(s, vocab::SH_NODE_KIND) {
298            if let Some(set) = map_node_kind(&k) {
299                let id = self.arena.insert(Shape::TestKind(set));
300                value.push(id);
301            } else {
302                self.diag(DiagLevel::Warning, "unrecognized sh:nodeKind value", s);
303            }
304        }
305
306        // numeric range (combine the four bounds into one facet)
307        let lo = self
308            .lit(s, vocab::SH_MIN_INCLUSIVE)
309            .map(|value| Bound {
310                value,
311                inclusive: true,
312            })
313            .or_else(|| {
314                self.lit(s, vocab::SH_MIN_EXCLUSIVE).map(|value| Bound {
315                    value,
316                    inclusive: false,
317                })
318            });
319        let hi = self
320            .lit(s, vocab::SH_MAX_INCLUSIVE)
321            .map(|value| Bound {
322                value,
323                inclusive: true,
324            })
325            .or_else(|| {
326                self.lit(s, vocab::SH_MAX_EXCLUSIVE).map(|value| Bound {
327                    value,
328                    inclusive: false,
329                })
330            });
331        if lo.is_some() || hi.is_some() {
332            let id = self
333                .arena
334                .insert(Shape::TestType(ValueType::NumericRange { lo, hi }));
335            value.push(id);
336        }
337
338        // length
339        let min_len = self.int(s, vocab::SH_MIN_LENGTH);
340        let max_len = self.int(s, vocab::SH_MAX_LENGTH);
341        if min_len.is_some() || max_len.is_some() {
342            let id = self.arena.insert(Shape::TestType(ValueType::Length {
343                min: min_len,
344                max: max_len,
345            }));
346            value.push(id);
347        }
348
349        // pattern (+ flags)
350        let flags = self
351            .lit(s, vocab::SH_FLAGS)
352            .map(|l| l.value().to_string())
353            .unwrap_or_default();
354        for pat in self.g.objects(s, vocab::SH_PATTERN) {
355            if let Term::Literal(l) = pat {
356                let id = self.arena.insert(Shape::TestType(ValueType::Pattern {
357                    regex: l.value().to_string(),
358                    flags: flags.clone(),
359                }));
360                value.push(id);
361            }
362        }
363
364        // sh:languageIn
365        for li in self.g.objects(s, vocab::SH_LANGUAGE_IN) {
366            let langs: Vec<String> = self
367                .g
368                .read_list(&li)
369                .into_iter()
370                .filter_map(|m| match m {
371                    Term::Literal(l) => Some(l.value().to_string()),
372                    _ => None,
373                })
374                .collect();
375            let id = self.arena.insert(Shape::TestType(ValueType::LangIn(langs)));
376            value.push(id);
377        }
378
379        // sh:in  ≡  ⋁ test(member)
380        for inl in self.g.objects(s, vocab::SH_IN) {
381            let alts: Vec<ShapeId> = self
382                .g
383                .read_list(&inl)
384                .into_iter()
385                .map(|m| self.arena.insert(Shape::TestConst(m)))
386                .collect();
387            let or = self.arena.or(alts);
388            value.push(or);
389        }
390
391        // sh:node — each value node must conform to the referenced shape
392        for n in self.g.objects(s, vocab::SH_NODE) {
393            if let Some(nn) = term_to_node(&n) {
394                let id = self.lower_shape(&nn);
395                value.push(id);
396            }
397        }
398
399        // sh:property — like sh:node, each *value node* must conform to the
400        // referenced property shape (so on a property shape it is scoped under
401        // ∀path, not applied to the focus node directly).
402        for prop in self.g.objects(s, vocab::SH_PROPERTY) {
403            if let Some(pn) = term_to_node(&prop) {
404                let id = self.lower_shape(&pn);
405                value.push(id);
406            }
407        }
408
409        // sh:not
410        for n in self.g.objects(s, vocab::SH_NOT) {
411            if let Some(nn) = term_to_node(&n) {
412                let id = self.lower_shape(&nn);
413                let neg = self.arena.not(id);
414                value.push(neg);
415            }
416        }
417
418        // sh:and / sh:or / sh:xone (each object is an rdf:list of shapes)
419        for l in self.g.objects(s, vocab::SH_AND) {
420            let ids = self.lower_shape_list(&l);
421            let a = self.arena.and(ids);
422            value.push(a);
423        }
424        for l in self.g.objects(s, vocab::SH_OR) {
425            let ids = self.lower_shape_list(&l);
426            let o = self.arena.or(ids);
427            value.push(o);
428        }
429        for l in self.g.objects(s, vocab::SH_XONE) {
430            let ids = self.lower_shape_list(&l);
431            let x = self.arena.xone(ids);
432            value.push(x);
433        }
434
435        value
436    }
437
438    /// Path-level constraints (cardinality, qualified counts, property pairs,
439    /// hasValue, uniqueLang). Most require a path; without one they are ignored
440    /// with a diagnostic, except `sh:hasValue` which applies to the focus node.
441    fn collect_path_constraints(
442        &mut self,
443        s: &NamedOrBlankNode,
444        path: Option<&Path>,
445        conjuncts: &mut Vec<ShapeId>,
446    ) {
447        let need_path = |me: &mut Self, what: &str| {
448            me.diag(DiagLevel::Warning, format!("{what} ignored: no sh:path"), s);
449        };
450
451        let min_count = self.int(s, vocab::SH_MIN_COUNT);
452        let max_count = self.int(s, vocab::SH_MAX_COUNT);
453        if min_count.is_some() || max_count.is_some() {
454            match path {
455                Some(p) => {
456                    let top = self.arena.top();
457                    let c = self.arena.count(p.clone(), min_count, max_count, top);
458                    conjuncts.push(c);
459                }
460                None => need_path(self, "sh:minCount/sh:maxCount"),
461            }
462        }
463
464        // sh:hasValue
465        for v in self.g.objects(s, vocab::SH_HAS_VALUE) {
466            match path {
467                Some(p) => {
468                    let tc = self.arena.insert(Shape::TestConst(v));
469                    let c = self.arena.count(p.clone(), Some(1), None, tc);
470                    conjuncts.push(c);
471                }
472                None => {
473                    let tc = self.arena.insert(Shape::TestConst(v));
474                    conjuncts.push(tc);
475                }
476            }
477        }
478
479        // sh:qualifiedValueShape + qualifiedMin/MaxCount
480        for q in self.g.objects(s, vocab::SH_QUALIFIED_VALUE_SHAPE) {
481            if let Some(qn) = term_to_node(&q) {
482                let qmin = self.int(s, vocab::SH_QUALIFIED_MIN_COUNT);
483                let qmax = self.int(s, vocab::SH_QUALIFIED_MAX_COUNT);
484                match path {
485                    Some(p) => {
486                        let mut qualifiers = vec![self.lower_shape(&qn)];
487                        if self.bool_prop(s, vocab::SH_QUALIFIED_VALUE_SHAPES_DISJOINT) {
488                            for sibling in self.sibling_qualified_shapes(s, &qn) {
489                                let sibling = self.lower_shape(&sibling);
490                                qualifiers.push(self.arena.not(sibling));
491                            }
492                        }
493                        let qualifier = self.arena.and(qualifiers);
494                        let c = self.arena.count(p.clone(), qmin, qmax, qualifier);
495                        conjuncts.push(c);
496                    }
497                    None => need_path(self, "sh:qualifiedValueShape"),
498                }
499            }
500        }
501
502        // property-pair constraints
503        let pairs = [
504            (vocab::SH_EQUALS, "equals"),
505            (vocab::SH_DISJOINT, "disjoint"),
506            (vocab::SH_LESS_THAN, "lessThan"),
507            (vocab::SH_LESS_THAN_OR_EQUALS, "lessThanOrEquals"),
508        ];
509        for (pred, name) in pairs {
510            for other in self.g.objects(s, pred) {
511                let Term::NamedNode(op) = other else { continue };
512                match path {
513                    Some(p) => {
514                        let shape = match name {
515                            "equals" => Shape::Eq(p.clone(), op),
516                            "disjoint" => Shape::Disj(p.clone(), op),
517                            "lessThan" => Shape::Lt(p.clone(), op),
518                            _ => Shape::Le(p.clone(), op),
519                        };
520                        let c = self.arena.insert(shape);
521                        conjuncts.push(c);
522                    }
523                    None if matches!(name, "equals" | "disjoint") => {
524                        let shape = if name == "equals" {
525                            Shape::Eq(Path::Id, op)
526                        } else {
527                            Shape::Disj(Path::Id, op)
528                        };
529                        let c = self.arena.insert(shape);
530                        conjuncts.push(c);
531                    }
532                    None => need_path(self, &format!("sh:{name}")),
533                }
534            }
535        }
536
537        // sh:uniqueLang
538        if self.bool_prop(s, vocab::SH_UNIQUE_LANG) {
539            match path {
540                Some(p) => {
541                    let c = self.arena.insert(Shape::UniqueLang(p.clone()));
542                    conjuncts.push(c);
543                }
544                None => need_path(self, "sh:uniqueLang"),
545            }
546        }
547    }
548
549    /// The target selectors of a shape (used by both its statements and rules).
550    fn target_selectors(&mut self, s: &NamedOrBlankNode) -> Vec<Selector> {
551        let mut sels = Vec::new();
552
553        for c in self.g.objects(s, vocab::SH_TARGET_NODE) {
554            sels.push(Selector::IsConst(c));
555        }
556        for c in self.g.objects(s, vocab::SH_TARGET_CLASS) {
557            sels.push(self.class_selector(c));
558        }
559        for p in self.g.objects(s, vocab::SH_TARGET_SUBJECTS_OF) {
560            if let Term::NamedNode(n) = p {
561                sels.push(Selector::HasOut(n));
562            }
563        }
564        for p in self.g.objects(s, vocab::SH_TARGET_OBJECTS_OF) {
565            if let Term::NamedNode(n) = p {
566                sels.push(Selector::HasIn(n));
567            }
568        }
569
570        // implicit class target: a shape that is also an rdfs:Class / owl:Class
571        if (self.g.is_instance_of(s, vocab::RDFS_CLASS)
572            || self.g.is_instance_of(s, vocab::OWL_CLASS))
573            && let NamedOrBlankNode::NamedNode(n) = s
574        {
575            sels.push(self.class_selector(Term::NamedNode(n.clone())));
576        }
577
578        for target_term in self.g.objects(s, vocab::SH_TARGET) {
579            let Some(target_node) = term_to_node(&target_term) else {
580                self.diag(DiagLevel::Error, "sh:target must reference a resource", s);
581                continue;
582            };
583            match self.g.object(&target_node, vocab::SH_SELECT) {
584                Some(Term::Literal(query)) => {
585                    if let Some(query) =
586                        self.canonical_sparql(&target_node, query.value(), ExpectedQuery::Select)
587                    {
588                        sels.push(Selector::Sparql(SparqlTarget { query }));
589                    }
590                }
591                _ => self.diag(
592                    DiagLevel::Unsupported,
593                    "custom sh:target without sh:select is not yet lowered",
594                    &target_node,
595                ),
596            }
597        }
598
599        sels
600    }
601
602    /// Lower SPARQL-based custom constraint components (SHACL §6.3) into the
603    /// algebra. For each shape that *activates* a component (supplies a value for
604    /// every mandatory parameter), a `Shape::Sparql` node is appended to that
605    /// shape's conjunction. The parameter values from the activating shape are
606    /// stored as `extra_bindings` so `compile_constraint` can pre-substitute them
607    /// (§6.2.3) before binding `$this` per focus node.
608    ///
609    /// Components with an invalid SPARQL validator query emit `DiagLevel::Error`
610    /// and are skipped (the constraint is not enforced for that activation).
611    fn lower_custom_components(&mut self, shapes: &[NamedOrBlankNode]) {
612        let components = self.collect_component_defs();
613        if components.is_empty() {
614            return;
615        }
616        for s in shapes {
617            let Some(&shape_id) = self.cache.get(s) else {
618                continue;
619            };
620            let is_property_shape = self.g.object(s, vocab::SH_PATH).is_some();
621            let path = if is_property_shape {
622                self.parse_shape_path(s)
623            } else {
624                None
625            };
626            let shape_term = match s {
627                NamedOrBlankNode::NamedNode(n) => Some(Term::NamedNode(n.clone())),
628                NamedOrBlankNode::BlankNode(b) => Some(Term::BlankNode(b.clone())),
629            };
630
631            for component in &components {
632                // Activation: every mandatory parameter must be present on `s`.
633                let mut extra_bindings: Vec<(String, Term)> = Vec::new();
634                let mut activated = true;
635                for p in &component.params {
636                    if let Some(value) = self.g.object(s, p.path.as_ref()) {
637                        extra_bindings.push((p.var.clone(), value));
638                    } else if !p.optional {
639                        activated = false;
640                        break;
641                    }
642                }
643                if !activated {
644                    continue;
645                }
646
647                // Pick the right validator for the shape kind.
648                let validator = if is_property_shape {
649                    component
650                        .property_validator
651                        .as_ref()
652                        .or(component.generic_validator.as_ref())
653                } else {
654                    component
655                        .node_validator
656                        .as_ref()
657                        .or(component.generic_validator.as_ref())
658                };
659                let Some(validator) = validator else { continue };
660
661                let constraint = SparqlConstraint {
662                    kind: validator.kind,
663                    query: validator.query.clone(),
664                    path: path.clone(),
665                    shape: shape_term.clone(),
666                    messages: validator.messages.clone(),
667                    extra_bindings,
668                    // For node-shape activations (no sh:path), $value equals the
669                    // focus node, so rename ?value → ?this before binding per focus.
670                    bind_value_to_this: !is_property_shape
671                        && validator.kind == SparqlQueryKind::Ask,
672                };
673                let raw_id = self.arena.insert(Shape::Sparql(constraint));
674                // sh:SPARQLAskValidator (SHACL §6.2.3): ASK=true means the
675                // constraint is *satisfied*. The algebra evaluator treats
676                // ASK=true as a violation (matching sh:sparql semantics), so
677                // we wrap in Not to restore the correct polarity. SELECT
678                // validators are violation-per-row, same as sh:sparql — no inversion.
679                let sparql_id = if validator.kind == SparqlQueryKind::Ask {
680                    self.arena.not(raw_id)
681                } else {
682                    raw_id
683                };
684
685                // Conjoin with the shape's existing body.
686                let (severity, inner) = match self.arena.get(shape_id) {
687                    Shape::Annotated {
688                        severity,
689                        shape: inner,
690                    } => (severity.clone(), *inner),
691                    _ => continue, // defensive; lower_shape always produces Annotated
692                };
693                let combined = self.arena.and(vec![inner, sparql_id]);
694                self.arena.set(
695                    shape_id,
696                    Shape::Annotated {
697                        severity,
698                        shape: combined,
699                    },
700                );
701            }
702        }
703    }
704
705    /// Collect all custom constraint component definitions from the shapes graph:
706    /// named subjects that carry `sh:parameter` and at least one validator
707    /// predicate (`sh:validator`, `sh:nodeValidator`, `sh:propertyValidator`).
708    fn collect_component_defs(&mut self) -> Vec<ComponentDef> {
709        let mut out: Vec<ComponentDef> = Vec::new();
710        let mut seen = HashSet::new();
711        for triple in self.g.graph.triples_for_predicate(vocab::SH_PARAMETER) {
712            let subject = triple.subject.into_owned();
713            if !seen.insert(subject.clone()) {
714                continue;
715            }
716            let NamedOrBlankNode::NamedNode(iri) = &subject else {
717                continue;
718            };
719            let node_validator = self
720                .g
721                .object(&subject, vocab::SH_NODE_VALIDATOR)
722                .and_then(|v| self.parse_validator_def(&v));
723            let property_validator = self
724                .g
725                .object(&subject, vocab::SH_PROPERTY_VALIDATOR)
726                .and_then(|v| self.parse_validator_def(&v));
727            let generic_validator = self
728                .g
729                .object(&subject, vocab::SH_VALIDATOR)
730                .and_then(|v| self.parse_validator_def(&v));
731            if node_validator.is_none()
732                && property_validator.is_none()
733                && generic_validator.is_none()
734            {
735                continue; // not a constraint component (e.g. a sh:SPARQLFunction)
736            }
737            let mut params: Vec<ParamDef> = Vec::new();
738            for p in self.g.objects(&subject, vocab::SH_PARAMETER) {
739                let Some(pn) = term_to_node(&p) else { continue };
740                let Some(Term::NamedNode(path)) = self.g.object(&pn, vocab::SH_PATH) else {
741                    continue;
742                };
743                let var = local_name(path.as_str()).to_string();
744                let optional = matches!(
745                    self.g.object(&pn, vocab::SH_OPTIONAL),
746                    Some(Term::Literal(ref l)) if l.value() == "true"
747                );
748                params.push(ParamDef {
749                    path,
750                    var,
751                    optional,
752                });
753            }
754            if params.is_empty() {
755                continue;
756            }
757            out.push(ComponentDef {
758                iri: iri.clone(),
759                params,
760                node_validator,
761                property_validator,
762                generic_validator,
763            });
764        }
765        out.sort_by(|a, b| a.iri.as_str().cmp(b.iri.as_str()));
766        out
767    }
768
769    /// Parse a validator node into a `ValidatorDef`, canonicalizing its SPARQL
770    /// query. Returns `None` (and emits an error diagnostic) if the query is
771    /// invalid SPARQL.
772    fn parse_validator_def(&mut self, node: &Term) -> Option<ValidatorDef> {
773        let node = term_to_node(node)?;
774        let (kind, raw) = if let Some(Term::Literal(q)) = self.g.object(&node, vocab::SH_ASK) {
775            (SparqlQueryKind::Ask, q.value().to_string())
776        } else if let Some(Term::Literal(q)) = self.g.object(&node, vocab::SH_SELECT) {
777            (SparqlQueryKind::Select, q.value().to_string())
778        } else {
779            return None; // no query body — not a SPARQL validator
780        };
781        let canonical = match canonical_sparql_query(self.g, &node, &raw) {
782            Ok((_, c)) => c,
783            Err(msg) => {
784                self.diag(
785                    DiagLevel::Error,
786                    format!("invalid SPARQL in validator: {msg}"),
787                    &node,
788                );
789                return None;
790            }
791        };
792        let messages = self.g.objects(&node, vocab::SH_MESSAGE);
793        Some(ValidatorDef {
794            kind,
795            query: canonical,
796            messages,
797        })
798    }
799
800    /// Lower the `sh:rule`s of a shape (SHACL-AF). A rule fires on the shape's
801    /// targets, so we emit one [`Rule`] per selector.
802    fn parse_rules(&mut self, s: &NamedOrBlankNode, selectors: &[Selector]) {
803        for rule_term in self.g.objects(s, vocab::SH_RULE) {
804            let Some(rn) = term_to_node(&rule_term) else {
805                continue;
806            };
807            let Some(head) = self.parse_rule_head(&rn) else {
808                continue;
809            };
810
811            let conditions: Vec<ShapeId> = self
812                .g
813                .objects(&rn, vocab::SH_CONDITION)
814                .iter()
815                .filter_map(term_to_node)
816                .map(|c| self.lower_shape(&c))
817                .collect();
818            let order = self.order(&rn);
819            let deactivated = self.bool_prop(&rn, vocab::SH_DEACTIVATED);
820
821            for sel in selectors {
822                self.rules.push(Rule {
823                    selector: sel.clone(),
824                    conditions: conditions.clone(),
825                    head: head.clone(),
826                    order,
827                    deactivated,
828                });
829            }
830        }
831    }
832
833    /// Qualified value shapes attached through the same parent `sh:property`
834    /// declaration, excluding the current qualified shape itself.
835    fn sibling_qualified_shapes(
836        &self,
837        shape: &NamedOrBlankNode,
838        qualifier: &NamedOrBlankNode,
839    ) -> Vec<NamedOrBlankNode> {
840        let mut siblings = HashSet::new();
841        for triple in self.g.graph.triples_for_predicate(vocab::SH_PROPERTY) {
842            if term_to_node(&triple.object.into_owned()).as_ref() != Some(shape) {
843                continue;
844            }
845            let parent = triple.subject.into_owned();
846            for property in self.g.objects(&parent, vocab::SH_PROPERTY) {
847                let Some(property) = term_to_node(&property) else {
848                    continue;
849                };
850                for sibling in self.g.objects(&property, vocab::SH_QUALIFIED_VALUE_SHAPE) {
851                    if let Some(sibling) = term_to_node(&sibling) {
852                        siblings.insert(sibling);
853                    }
854                }
855            }
856        }
857        siblings.remove(qualifier);
858        let mut siblings: Vec<_> = siblings.into_iter().collect();
859        siblings.sort_by_key(|node| node.to_string());
860        siblings
861    }
862
863    fn parse_rule_head(&mut self, rn: &NamedOrBlankNode) -> Option<RuleHead> {
864        // sh:SPARQLRule — parse and canonicalize the CONSTRUCT while retaining
865        // an opaque algebra leaf for later query rewriting.
866        if let Some(Term::Literal(q)) = self.g.object(rn, vocab::SH_CONSTRUCT) {
867            let query = self.canonical_sparql(rn, q.value(), ExpectedQuery::Construct)?;
868            return Some(RuleHead::Sparql(SparqlConstruct { query }));
869        }
870        // sh:TripleRule — subject/predicate/object node expressions
871        let (subj, pred, obj) = (
872            self.g.object(rn, vocab::SH_SUBJECT),
873            self.g.object(rn, vocab::SH_PREDICATE),
874            self.g.object(rn, vocab::SH_OBJECT),
875        );
876        if subj.is_none() && pred.is_none() && obj.is_none() {
877            self.diag(DiagLevel::Unsupported, "unrecognized sh:rule head", rn);
878            return None;
879        }
880        let (Some(subj), Some(pred), Some(obj)) = (subj, pred, obj) else {
881            self.diag(
882                DiagLevel::Error,
883                "sh:TripleRule missing subject/predicate/object",
884                rn,
885            );
886            return None;
887        };
888        Some(RuleHead::Triple {
889            subject: self.parse_node_expr(subj, rn)?,
890            predicate: self.parse_node_expr(pred, rn)?,
891            object: self.parse_node_expr(obj, rn)?,
892        })
893    }
894
895    /// Parse a node expression (SHACL-AF §6). Handles `sh:this`, constants,
896    /// path expressions, filter / intersection / union expressions, and SPARQL
897    /// function calls `[ ex:fn (arg …) ]`.
898    fn parse_node_expr(&mut self, term: Term, owner: &NamedOrBlankNode) -> Option<NodeExpr> {
899        match &term {
900            Term::NamedNode(n) if n.as_ref() == vocab::SH_THIS => Some(NodeExpr::This),
901            Term::NamedNode(_) | Term::Literal(_) => Some(NodeExpr::Constant(term)),
902            Term::BlankNode(_) => {
903                let node = term_to_node(&term).expect("blank node");
904                if let Some(path_term) = self.g.object(&node, vocab::SH_PATH) {
905                    match parse_path(self.g, &path_term) {
906                        Ok(path) => Some(NodeExpr::Path(path)),
907                        Err(e) => {
908                            self.diag(
909                                DiagLevel::Error,
910                                format!("invalid node-expression path: {e}"),
911                                owner,
912                            );
913                            None
914                        }
915                    }
916                } else if self.g.object(&node, vocab::SH_FILTER_SHAPE).is_some() {
917                    self.parse_filter_expr(&node, owner)
918                } else if let Some(list) = self.g.object(&node, vocab::SH_INTERSECTION) {
919                    self.parse_set_expr(&list, owner)
920                        .map(NodeExpr::Intersection)
921                } else if let Some(list) = self.g.object(&node, vocab::SH_UNION) {
922                    self.parse_set_expr(&list, owner).map(NodeExpr::Union)
923                } else if let Some(expr) = self.try_function_call(&node, owner) {
924                    Some(expr)
925                } else {
926                    self.diag(
927                        DiagLevel::Unsupported,
928                        "complex node expression not yet lowered",
929                        owner,
930                    );
931                    None
932                }
933            }
934        }
935    }
936
937    /// `sh:filterShape` + `sh:nodes` — a filter node expression (SHACL-AF §6.4):
938    /// the value nodes of `sh:nodes` that conform to `sh:filterShape`.
939    fn parse_filter_expr(
940        &mut self,
941        node: &NamedOrBlankNode,
942        owner: &NamedOrBlankNode,
943    ) -> Option<NodeExpr> {
944        let Some(shape_term) = self.g.object(node, vocab::SH_FILTER_SHAPE) else {
945            self.diag(DiagLevel::Error, "sh:filterShape missing a value", owner);
946            return None;
947        };
948        let Some(shape_node) = term_to_node(&shape_term) else {
949            self.diag(
950                DiagLevel::Error,
951                "sh:filterShape must reference a shape",
952                owner,
953            );
954            return None;
955        };
956        let Some(nodes_term) = self.g.object(node, vocab::SH_NODES) else {
957            self.diag(
958                DiagLevel::Error,
959                "filter expression missing sh:nodes",
960                owner,
961            );
962            return None;
963        };
964        let input = self.parse_node_expr(nodes_term, owner)?;
965        let shape = self.lower_shape(&shape_node);
966        Some(NodeExpr::Filter {
967            input: Box::new(input),
968            shape,
969        })
970    }
971
972    /// Parse the RDF list of an `sh:intersection` / `sh:union` node expression
973    /// (SHACL-AF §6.5–6.6) into its member node expressions. Returns `None` if
974    /// the list is empty or any member fails to parse (already diagnosed).
975    fn parse_set_expr(
976        &mut self,
977        list_head: &Term,
978        owner: &NamedOrBlankNode,
979    ) -> Option<Vec<NodeExpr>> {
980        let members = self.g.read_list(list_head);
981        if members.is_empty() {
982            self.diag(
983                DiagLevel::Error,
984                "sh:intersection/sh:union expects a non-empty list",
985                owner,
986            );
987            return None;
988        }
989        let n = members.len();
990        let exprs: Vec<NodeExpr> = members
991            .into_iter()
992            .filter_map(|t| self.parse_node_expr(t, owner))
993            .collect();
994        if exprs.len() != n {
995            return None;
996        }
997        Some(exprs)
998    }
999
1000    /// Detect a function-call node expression `[ ex:fn ( arg1 arg2 … ) ]`.
1001    ///
1002    /// The blank node must have exactly one non-SHACL/RDF/RDFS/OWL predicate;
1003    /// its object must be an RDF list of argument node expressions.
1004    fn try_function_call(
1005        &mut self,
1006        node: &NamedOrBlankNode,
1007        owner: &NamedOrBlankNode,
1008    ) -> Option<NodeExpr> {
1009        let func_preds: Vec<(NamedNode, Term)> = self
1010            .g
1011            .graph
1012            .triples_for_subject(node)
1013            .map(|t| (t.predicate.into_owned(), t.object.into_owned()))
1014            .filter(|(p, _)| {
1015                let s = p.as_str();
1016                !s.starts_with(vocab::SH)
1017                    && !s.starts_with(vocab::RDF)
1018                    && !s.starts_with(vocab::RDFS)
1019                    && !s.starts_with(vocab::OWL)
1020            })
1021            .collect();
1022
1023        if func_preds.len() != 1 {
1024            return None;
1025        }
1026        let (func_iri, list_head) = func_preds.into_iter().next().unwrap();
1027        let arg_terms = self.g.read_list(&list_head);
1028        let n = arg_terms.len();
1029        let args: Vec<NodeExpr> = arg_terms
1030            .into_iter()
1031            .filter_map(|t| self.parse_node_expr(t, owner))
1032            .collect();
1033        if args.len() != n {
1034            return None;
1035        }
1036        Some(NodeExpr::Function {
1037            iri: func_iri,
1038            args,
1039        })
1040    }
1041
1042    fn order(&self, s: &NamedOrBlankNode) -> Option<i64> {
1043        match self.g.object(s, vocab::SH_ORDER) {
1044            Some(Term::Literal(l)) => l.value().parse().ok(),
1045            _ => None,
1046        }
1047    }
1048
1049    /// `∃≥1 (rdf:type/rdfs:subClassOf*) . test(class)` as a selector.
1050    fn class_selector(&mut self, class: Term) -> Selector {
1051        let tn = self.arena.insert(Shape::TestConst(class));
1052        Selector::HasPath(class_path(), tn)
1053    }
1054
1055    fn lower_shape_list(&mut self, list_head: &Term) -> Vec<ShapeId> {
1056        self.g
1057            .read_list(list_head)
1058            .into_iter()
1059            .filter_map(|m| term_to_node(&m))
1060            .map(|n| self.lower_shape(&n))
1061            .collect()
1062    }
1063
1064    fn parse_shape_path(&mut self, s: &NamedOrBlankNode) -> Option<Path> {
1065        let term = self.g.object(s, vocab::SH_PATH)?;
1066        match parse_path(self.g, &term) {
1067            Ok(p) => Some(p),
1068            Err(e) => {
1069                self.diag(DiagLevel::Error, format!("invalid sh:path: {e}"), s);
1070                None
1071            }
1072        }
1073    }
1074
1075    fn closed_allowed(&self, s: &NamedOrBlankNode) -> BTreeSet<oxrdf::NamedNode> {
1076        let mut q = BTreeSet::new();
1077        for prop in self.g.objects(s, vocab::SH_PROPERTY) {
1078            if let Some(pn) = term_to_node(&prop)
1079                && let Some(Term::NamedNode(n)) = self.g.object(&pn, vocab::SH_PATH)
1080            {
1081                q.insert(n);
1082            }
1083        }
1084        for ip in self.g.objects(s, vocab::SH_IGNORED_PROPERTIES) {
1085            for m in self.g.read_list(&ip) {
1086                if let Term::NamedNode(n) = m {
1087                    q.insert(n);
1088                }
1089            }
1090        }
1091        q
1092    }
1093
1094    fn bool_prop(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> bool {
1095        matches!(self.g.object(s, pred), Some(Term::Literal(l)) if l.value() == "true")
1096    }
1097
1098    fn int(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<u64> {
1099        match self.g.object(s, pred) {
1100            Some(Term::Literal(l)) => l.value().parse().ok(),
1101            _ => None,
1102        }
1103    }
1104
1105    fn lit(&self, s: &NamedOrBlankNode, pred: oxrdf::NamedNodeRef) -> Option<Literal> {
1106        match self.g.object(s, pred) {
1107            Some(Term::Literal(l)) => Some(l),
1108            _ => None,
1109        }
1110    }
1111
1112    /// Parse a SHACL SPARQL query once, resolving both document prefixes and
1113    /// `sh:prefixes` declarations. `Query::to_string` expands prefix names, so
1114    /// the IR remains self-contained and can be reparsed or rewritten later.
1115    fn canonical_sparql(
1116        &mut self,
1117        owner: &NamedOrBlankNode,
1118        raw: &str,
1119        expected: ExpectedQuery,
1120    ) -> Option<String> {
1121        let (query, canonical) = match canonical_sparql_query(self.g, owner, raw) {
1122            Ok(result) => result,
1123            Err(message) => {
1124                self.diag(DiagLevel::Error, message, owner);
1125                return None;
1126            }
1127        };
1128        let actual = match &query {
1129            Query::Select { .. } => ExpectedQuery::Select,
1130            Query::Ask { .. } => ExpectedQuery::Ask,
1131            Query::Construct { .. } => ExpectedQuery::Construct,
1132            Query::Describe { .. } => ExpectedQuery::Describe,
1133        };
1134        if actual != expected {
1135            self.diag(
1136                DiagLevel::Error,
1137                format!("expected SPARQL {expected}, found {actual}"),
1138                owner,
1139            );
1140            return None;
1141        }
1142        Some(canonical)
1143    }
1144}
1145
1146/// Build the canonical, prefix-expanded form of a SHACL SPARQL query string.
1147///
1148/// Resolves the document base IRI, document-level prefixes, and the
1149/// `sh:prefixes` / `sh:declare` chains (following `owl:imports`) declared on
1150/// `owner`, parses `raw`, and returns the parsed query together with its
1151/// canonical string form. `Query::to_string` expands prefix names, so the
1152/// result is self-contained and can be reparsed without external declarations.
1153///
1154/// Errors are returned as messages so callers can decide how to surface them:
1155/// the lowerer routes them to diagnostics; the report validator drops the
1156/// offending constraint, matching the lowering path.
1157pub fn canonical_sparql_query(
1158    g: &Loaded,
1159    owner: &NamedOrBlankNode,
1160    raw: &str,
1161) -> Result<(Query, String), String> {
1162    let mut parser = SparqlParser::new();
1163    if let Some(base) = &g.base {
1164        parser = parser
1165            .with_base_iri(base)
1166            .map_err(|e| format!("invalid SPARQL base IRI: {e}"))?;
1167    }
1168    for (prefix, namespace) in &g.prefixes {
1169        parser = parser
1170            .with_prefix(prefix, namespace)
1171            .map_err(|e| format!("invalid SPARQL prefix declaration {prefix}: {e}"))?;
1172    }
1173    let mut prefix_sources: Vec<NamedOrBlankNode> = g
1174        .objects(owner, vocab::SH_PREFIXES)
1175        .iter()
1176        .filter_map(term_to_node)
1177        .collect();
1178    let mut seen_sources = HashSet::new();
1179    while let Some(source) = prefix_sources.pop() {
1180        if !seen_sources.insert(source.clone()) {
1181            continue;
1182        }
1183        prefix_sources.extend(
1184            g.objects(&source, vocab::OWL_IMPORTS)
1185                .iter()
1186                .filter_map(term_to_node),
1187        );
1188        for declaration_term in g.objects(&source, vocab::SH_DECLARE) {
1189            let Some(declaration) = term_to_node(&declaration_term) else {
1190                continue;
1191            };
1192            let (Some(Term::Literal(prefix)), Some(Term::Literal(namespace))) = (
1193                g.object(&declaration, vocab::SH_PREFIX),
1194                g.object(&declaration, vocab::SH_NAMESPACE),
1195            ) else {
1196                continue;
1197            };
1198            parser = parser
1199                .with_prefix(prefix.value(), namespace.value())
1200                .map_err(|e| format!("invalid SHACL SPARQL prefix declaration: {e}"))?;
1201        }
1202    }
1203    let query = parser
1204        .parse_query(raw)
1205        .map_err(|e| format!("invalid SPARQL query: {e}"))?;
1206    let canonical = query.to_string();
1207    Ok((query, canonical))
1208}
1209
1210#[derive(Clone, Copy, PartialEq, Eq)]
1211enum ExpectedQuery {
1212    Select,
1213    Ask,
1214    Construct,
1215    Describe,
1216}
1217
1218impl std::fmt::Display for ExpectedQuery {
1219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1220        f.write_str(match self {
1221            Self::Select => "SELECT",
1222            Self::Ask => "ASK",
1223            Self::Construct => "CONSTRUCT",
1224            Self::Describe => "DESCRIBE",
1225        })
1226    }
1227}
1228
1229fn class_path() -> Path {
1230    Path::seq(vec![
1231        Path::Pred(vocab::rdf_type()),
1232        Path::star(Path::Pred(vocab::rdfs_subclassof())),
1233    ])
1234}
1235
1236/// Whether a node expression contains a SPARQL function application anywhere in
1237/// its tree. Such expressions cannot yet be evaluated in the validation paths
1238/// (they need shapes-graph function lookups), so expression constraints over
1239/// them are diagnosed rather than lowered.
1240fn node_expr_has_function(e: &NodeExpr) -> bool {
1241    match e {
1242        NodeExpr::Function { .. } => true,
1243        NodeExpr::Filter { input, .. } => node_expr_has_function(input),
1244        NodeExpr::Intersection(es) | NodeExpr::Union(es) => es.iter().any(node_expr_has_function),
1245        NodeExpr::This | NodeExpr::Constant(_) | NodeExpr::Path(_) => false,
1246    }
1247}
1248
1249fn map_node_kind(term: &Term) -> Option<NodeKindSet> {
1250    let Term::NamedNode(n) = term else {
1251        return None;
1252    };
1253    let r = n.as_ref();
1254    Some(if r == vocab::SH_IRI {
1255        NodeKindSet::IRI
1256    } else if r == vocab::SH_BLANK_NODE {
1257        NodeKindSet::BLANK_NODE
1258    } else if r == vocab::SH_LITERAL {
1259        NodeKindSet::LITERAL
1260    } else if r == vocab::SH_BLANK_NODE_OR_IRI {
1261        NodeKindSet::BLANK_NODE_OR_IRI
1262    } else if r == vocab::SH_BLANK_NODE_OR_LITERAL {
1263        NodeKindSet::BLANK_NODE_OR_LITERAL
1264    } else if r == vocab::SH_IRI_OR_LITERAL {
1265        NodeKindSet::IRI_OR_LITERAL
1266    } else {
1267        return None;
1268    })
1269}