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