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