1use crate::frozen::FrozenIndexedDataset;
17use crate::path::{node_of, succ};
18use crate::sparql::{FunctionDef, SparqlExecutor, query_reads_graph};
19use crate::validate::{
20 EngineOptions, NonStratifiable, ShapeEvaluator, focus_nodes_with, graph_union,
21};
22use oxrdf::{Graph, NamedNode, NamedOrBlankNode, Term, Triple};
23use shifty_algebra::{NodeExpr, Rule, RuleHead, Schema, Selector, ShapeArena};
24use shifty_opt::{RuleDependencies, analyze, rule_dependencies, rule_guard_dependencies};
25use shifty_parse::vocab;
26use std::collections::{BTreeSet, HashMap, HashSet};
27
28pub struct InferenceOutcome {
30 pub graph: Graph,
32 pub inferred: Vec<Triple>,
34 pub diagnostics: Vec<String>,
36}
37
38pub fn infer(data: &Graph, schema: &Schema) -> Result<InferenceOutcome, NonStratifiable> {
40 infer_with_context(data, data, schema)
41}
42
43pub fn infer_with_options(
46 data: &Graph,
47 schema: &Schema,
48 options: &EngineOptions,
49) -> Result<InferenceOutcome, NonStratifiable> {
50 infer_with_context_and_options(data, data, schema, options)
51}
52
53pub fn infer_graphs(
58 data: &Graph,
59 shapes: &Graph,
60 schema: &Schema,
61) -> Result<InferenceOutcome, NonStratifiable> {
62 let context = graph_union(data, shapes);
63 infer_with_context(data, &context, schema)
64}
65
66pub fn infer_with_context(
70 data: &Graph,
71 context: &Graph,
72 schema: &Schema,
73) -> Result<InferenceOutcome, NonStratifiable> {
74 infer_with_context_and_options(data, context, schema, &EngineOptions::default())
75}
76
77pub fn infer_with_context_and_options(
79 data: &Graph,
80 context: &Graph,
81 schema: &Schema,
82 options: &EngineOptions,
83) -> Result<InferenceOutcome, NonStratifiable> {
84 let strat = analyze(&schema.arena);
85 if !strat.stratifiable {
86 let components = strat
87 .strata
88 .iter()
89 .filter(|s| !s.stratifiable)
90 .map(|s| s.shapes.clone())
91 .collect();
92 return Err(NonStratifiable { components });
93 }
94
95 let mut graph = data.clone();
96 let mut context = context.clone();
97 let mut sparql =
98 SparqlExecutor::new(&context).expect("building an in-memory Oxigraph store should succeed");
99 sparql.set_functions(collect_functions(&context), options.unsupported);
102 let mut inferred: Vec<Triple> = Vec::new();
103 let mut diags: BTreeSet<String> = BTreeSet::new();
104
105 let mut rules: Vec<ScheduledRule<'_>> = schema
106 .rules
107 .iter()
108 .enumerate()
109 .filter(|(_, rule)| !rule.deactivated)
110 .map(|(index, rule)| ScheduledRule {
111 index,
112 order: rule.order.unwrap_or(0),
113 dependencies: rule_dependencies(rule, &schema.arena),
114 guard_dependencies: rule_guard_dependencies(rule, &schema.arena),
115 rule,
116 })
117 .collect();
118 rules.sort_by_key(|scheduled| (scheduled.order, scheduled.index));
119 let mut frozen = rules
120 .iter()
121 .any(|scheduled| matches!(scheduled.rule.head, RuleHead::Sparql(_)))
122 .then(|| FrozenIndexedDataset::from_graph(&context));
123
124 let mut active: HashSet<usize> = (0..rules.len()).collect();
127 let mut delta_start = 0;
130 let mut first_pass = true;
131 loop {
132 let mut changed_predicates = HashSet::new();
133 let mut added = false;
134 let mut start = 0;
135 let pass_start = inferred.len();
136 let mut visible_changed: HashSet<NamedNode> = inferred[delta_start..]
137 .iter()
138 .map(|triple| triple.predicate.clone())
139 .collect();
140
141 let mut focus_cache: HashMap<Selector, Vec<Term>> = HashMap::new();
145 let mut pass_changed: HashSet<NamedNode> = HashSet::new();
148
149 while start < rules.len() {
150 let order = rules[start].order;
151 let mut end = start + 1;
152 while end < rules.len() && rules[end].order == order {
153 end += 1;
154 }
155
156 let mut candidates: HashSet<Triple> = HashSet::new();
161 for (position, scheduled) in rules[start..end].iter().enumerate() {
162 if !active.contains(&(start + position)) {
163 continue;
164 }
165 let sel = &scheduled.rule.selector;
166 if selector_stale(sel, &pass_changed) {
167 focus_cache.remove(sel);
168 }
169 let focus_nodes = focus_cache.entry(sel.clone()).or_insert_with(|| {
170 focus_nodes_with(&graph, &context, sel, &schema.arena, &sparql)
171 });
172 let mut delta_focus_nodes = Vec::new();
173 let execution_focus_nodes = match &scheduled.rule.head {
174 RuleHead::Sparql(construct)
175 if !first_pass
176 && !focus_nodes.is_empty()
177 && (inferred.len() - delta_start).saturating_mul(2)
181 < focus_nodes.len()
182 && !scheduled
183 .guard_dependencies
184 .affected_by(&visible_changed) =>
185 {
186 match sparql.construct_delta_foci(
187 &construct.query,
188 &inferred[delta_start..],
189 frozen.as_ref(),
190 ) {
191 Ok(Some(affected)) => {
192 delta_focus_nodes.extend(
193 focus_nodes
194 .iter()
195 .filter(|focus| affected.contains(*focus))
196 .cloned(),
197 );
198 delta_focus_nodes.as_slice()
199 }
200 Ok(None) | Err(_) => focus_nodes.as_slice(),
201 }
202 }
203 _ => focus_nodes.as_slice(),
204 };
205 let rule_label = format!("rule[{}]", start + position);
206 let rule_t = web_time::Instant::now();
207 fire_rule(
208 execution_focus_nodes,
209 &context,
210 &schema.arena,
211 scheduled.rule,
212 &sparql,
213 frozen.as_ref(),
214 &mut candidates,
215 &mut diags,
216 );
217 crate::profile::record_shape(&rule_label, rule_t.elapsed().as_micros() as u64);
218 }
219 if let Some(frozen) = frozen.as_mut() {
220 frozen.extend_triples(candidates.iter());
221 }
222 for t in candidates {
223 pass_changed.insert(t.predicate.clone());
224 visible_changed.insert(t.predicate.clone());
225 graph.insert(&t);
226 context.insert(&t);
227 if let Err(error) = sparql.insert(&t) {
228 diags.insert(format!("failed to update SPARQL inference store: {error}"));
229 }
230 changed_predicates.insert(t.predicate.clone());
231 inferred.push(t);
232 added = true;
233 }
234
235 start = end;
236 }
237
238 if !added {
239 break;
240 }
241
242 delta_start = pass_start;
243 first_pass = false;
244 active.clear();
245 for (position, scheduled) in rules.iter().enumerate() {
246 if scheduled.dependencies.affected_by(&changed_predicates) {
247 active.insert(position);
248 }
249 }
250 if active.is_empty() {
251 break;
252 }
253 }
254
255 Ok(InferenceOutcome {
256 graph,
257 inferred,
258 diagnostics: diags.into_iter().collect(),
259 })
260}
261
262struct ScheduledRule<'a> {
263 index: usize,
264 order: i64,
265 dependencies: RuleDependencies,
266 guard_dependencies: RuleDependencies,
267 rule: &'a Rule,
268}
269
270fn selector_stale(sel: &Selector, pass_changed: &HashSet<NamedNode>) -> bool {
273 if pass_changed.is_empty() {
274 return false;
275 }
276 match sel {
277 Selector::HasOut(p) | Selector::HasIn(p) => pass_changed.contains(p),
278 Selector::IsConst(_) => false,
279 Selector::HasPath(..) | Selector::Sparql(_) => true,
281 }
282}
283
284#[allow(clippy::too_many_arguments)]
285fn fire_rule(
286 focus_nodes: &[Term],
287 context: &Graph,
288 arena: &ShapeArena,
289 rule: &shifty_algebra::Rule,
290 sparql: &SparqlExecutor,
291 frozen: Option<&FrozenIndexedDataset>,
292 out: &mut HashSet<Triple>,
293 diags: &mut BTreeSet<String>,
294) {
295 let mut evaluator = ShapeEvaluator::new(context, arena, sparql);
296 let eligible: Vec<&Term> = focus_nodes
297 .iter()
298 .filter(|v| rule.conditions.iter().all(|c| evaluator.holds(v, *c)))
299 .collect();
300
301 match &rule.head {
302 RuleHead::Triple {
303 subject,
304 predicate,
305 object,
306 } => {
307 for v in eligible {
308 let subjects = eval_node_expr(context, v, subject, &mut evaluator, diags);
309 let predicates = eval_node_expr(context, v, predicate, &mut evaluator, diags);
310 let objects = eval_node_expr(context, v, object, &mut evaluator, diags);
311 for s in &subjects {
312 let Some(subj) = node_of(s) else { continue };
313 for p in &predicates {
314 let Term::NamedNode(pred) = p else { continue };
315 for o in &objects {
316 let t = Triple::new(subj.clone(), pred.clone(), o.clone());
317 if !context.contains(&t) {
318 out.insert(t);
319 }
320 }
321 }
322 }
323 }
324 }
325 RuleHead::Sparql(construct) => {
326 let eligible: Vec<Term> = eligible.into_iter().cloned().collect();
327 match sparql.construct_many(&construct.query, &eligible, frozen) {
328 Ok(triples) => {
329 for triple in triples {
330 if matches!(triple.subject, oxrdf::NamedOrBlankNode::BlankNode(_))
331 || matches!(triple.object, Term::BlankNode(_))
332 {
333 diags.insert(
334 "sh:SPARQLRule CONSTRUCT blank nodes are not supported because \
335 they can prevent fixpoint termination"
336 .to_string(),
337 );
338 } else {
339 out.insert(triple);
340 }
341 }
342 }
343 Err(error) => {
344 diags.insert(format!("sh:SPARQLRule evaluation failed: {error}"));
345 }
346 }
347 }
348 }
349}
350
351fn eval_node_expr(
353 g: &Graph,
354 v: &Term,
355 expr: &NodeExpr,
356 evaluator: &mut ShapeEvaluator<'_>,
357 diags: &mut BTreeSet<String>,
358) -> HashSet<Term> {
359 match expr {
360 NodeExpr::This => once(v.clone()),
361 NodeExpr::Constant(t) => once(t.clone()),
362 NodeExpr::Path(p) => succ(g, v, p),
363 NodeExpr::Filter { input, shape } => eval_node_expr(g, v, input, evaluator, diags)
364 .into_iter()
365 .filter(|x| evaluator.holds(x, *shape))
366 .collect(),
367 NodeExpr::Intersection(es) => {
368 let mut iter = es.iter();
369 match iter.next() {
370 Some(first) => {
371 let mut acc = eval_node_expr(g, v, first, evaluator, diags);
372 for e in iter {
373 let s = eval_node_expr(g, v, e, evaluator, diags);
374 acc.retain(|x| s.contains(x));
375 }
376 acc
377 }
378 None => HashSet::new(),
379 }
380 }
381 NodeExpr::Union(es) => {
382 let mut acc = HashSet::new();
383 for e in es {
384 acc.extend(eval_node_expr(g, v, e, evaluator, diags));
385 }
386 acc
387 }
388 NodeExpr::Function { iri, args } => {
389 let arg_values: Vec<HashSet<Term>> = args
391 .iter()
392 .map(|a| eval_node_expr(g, v, a, evaluator, diags))
393 .collect();
394
395 let func = NamedOrBlankNode::NamedNode(iri.clone());
396 let Some(query_text) = g
397 .object_for_subject_predicate(&func, vocab::SH_SELECT)
398 .map(|t| t.into_owned())
399 .and_then(|t| match t {
400 Term::Literal(l) => Some(l.value().to_string()),
401 _ => None,
402 })
403 else {
404 diags.insert(format!("function <{}> has no sh:select", iri.as_str()));
405 return HashSet::new();
406 };
407
408 let params = function_params(g, &func);
409 let sparql = evaluator.sparql();
410 let mut results = HashSet::new();
411 for combo in cartesian_product(&arg_values) {
412 if combo.len() != params.len() {
413 continue;
414 }
415 let bindings: Vec<(String, Term)> = params
416 .iter()
417 .zip(combo)
418 .map(|(name, val)| (name.clone(), val))
419 .collect();
420 match sparql.call_sparql_function(&query_text, &bindings) {
421 Ok(terms) => results.extend(terms),
422 Err(e) => {
423 diags.insert(format!("function <{}> error: {e}", iri.as_str()));
424 }
425 }
426 }
427 results
428 }
429 }
430}
431
432fn once(t: Term) -> HashSet<Term> {
433 let mut s = HashSet::with_capacity(1);
434 s.insert(t);
435 s
436}
437
438fn local_name(iri: &str) -> &str {
440 iri.rsplit(['#', '/']).next().unwrap_or(iri)
441}
442
443fn collect_functions(g: &Graph) -> Vec<FunctionDef> {
446 let mut out = Vec::new();
447 for func in g
448 .subjects_for_predicate_object(vocab::RDF_TYPE, vocab::SH_SPARQL_FUNCTION)
449 .map(|s| s.into_owned())
450 .collect::<Vec<_>>()
451 {
452 let NamedOrBlankNode::NamedNode(iri) = &func else {
453 continue;
454 };
455 let raw = match g
456 .object_for_subject_predicate(&func, vocab::SH_SELECT)
457 .or_else(|| g.object_for_subject_predicate(&func, vocab::SH_ASK))
458 .map(|t| t.into_owned())
459 {
460 Some(Term::Literal(l)) => l.value().to_string(),
461 _ => continue,
462 };
463 out.push(FunctionDef {
464 iri: iri.clone(),
465 params: function_params(g, &func),
466 reads_graph: query_reads_graph(&raw),
467 query: raw,
468 });
469 }
470 out
471}
472
473fn function_params(g: &Graph, func: &NamedOrBlankNode) -> Vec<String> {
476 let mut params: Vec<(i64, String)> = g
477 .objects_for_subject_predicate(func, vocab::SH_PARAMETER)
478 .filter_map(|param_ref| {
479 let param_node = node_of(¶m_ref.into_owned())?;
480 let order = g
481 .object_for_subject_predicate(¶m_node, vocab::SH_ORDER)
482 .map(|t| t.into_owned())
483 .and_then(|t| match t {
484 Term::Literal(l) => l.value().parse::<i64>().ok(),
485 _ => None,
486 })
487 .unwrap_or(0);
488 let name = g
489 .object_for_subject_predicate(¶m_node, vocab::SH_NAME)
490 .map(|t| t.into_owned())
491 .and_then(|t| match t {
492 Term::Literal(l) => Some(l.value().to_string()),
493 _ => None,
494 })
495 .or_else(|| {
496 g.object_for_subject_predicate(¶m_node, vocab::SH_PATH)
497 .map(|t| t.into_owned())
498 .and_then(|t| match t {
499 Term::NamedNode(n) => Some(local_name(n.as_str()).to_string()),
500 _ => None,
501 })
502 })?;
503 Some((order, name))
504 })
505 .collect();
506 params.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
507 params.into_iter().map(|(_, name)| name).collect()
508}
509
510fn cartesian_product(sets: &[HashSet<Term>]) -> Vec<Vec<Term>> {
512 sets.iter().fold(vec![vec![]], |acc, set| {
513 acc.into_iter()
514 .flat_map(|combo| {
515 set.iter().map(move |item| {
516 let mut row = combo.clone();
517 row.push(item.clone());
518 row
519 })
520 })
521 .collect()
522 })
523}