1use std::collections::{HashMap, HashSet, VecDeque};
6
7#[cfg(feature = "async")]
8use std::sync::Arc;
9#[cfg(feature = "web")]
10use storage::js::JsStorage;
11#[cfg(feature = "web")]
12use wasm_bindgen::prelude::*;
13
14use itertools::Itertools;
15use runtime::compiler::RuleCycleDetector;
16
17use crate::{parser::Predicate, runtime::AtomDisplayWrapper};
18
19pub mod library;
20pub mod parser;
21pub mod runtime;
22pub mod storage;
23
24pub use runtime::{
25 compiler::{Compiler, ProgramError},
26 Atom, BodyAtom, CompiledProgram, GroundedAtom, GroundedBodyAtom, GroundedGoal, GroundedTerm,
27 Rule, Term,
28};
29pub use storage::{Fixed, FixedStorage, Memoized, Storage, StorageRef, ThreadsafeStorageRef};
30
31fn fixed_point_expand(
32 facts: &[(String, Vec<GroundedTerm>)],
33 rules: &[Rule],
34 universe: &HashSet<GroundedTerm>,
35 other: &[&dyn Storage],
36) -> (Fixed, HashSet<GroundedTerm>) {
37 let mut total_facts = HashSet::new();
38
39 let mut iteration_facts: HashSet<_> = facts
41 .iter()
42 .map(|(name, terms)| GroundedAtom {
43 predicate: parser::Predicate {
44 is_intrinsic: false,
45 name: name.to_string(),
46 },
47 terms: terms.clone(),
48 })
49 .chain(
50 other
51 .iter()
52 .flat_map(|s| s.as_fixed())
53 .flat_map(|fs| fs.get_facts()),
54 )
55 .collect();
56 let mut counter = 0;
57
58 let universe: HashSet<_> = universe
60 .union(
61 &iteration_facts
62 .iter()
63 .flat_map(|atom| atom.terms.iter().cloned())
64 .collect(),
65 )
66 .cloned()
67 .collect();
68
69 let rule_edges: HashSet<_> = rules
71 .iter()
72 .flat_map(|rule| {
73 rule.body.iter().map(|from| {
74 (
75 from.atom().predicate.clone(),
76 Predicate {
77 is_intrinsic: false,
78 name: rule.head.0.clone(),
79 },
80 from.negative().is_some(),
81 )
82 })
83 })
84 .collect();
85
86 let rule_vertices: HashSet<_> = rules
87 .iter()
88 .flat_map(|rule| {
89 rule.body
90 .iter()
91 .map(|from| from.atom().predicate.clone())
92 .chain(std::iter::once(Predicate {
93 is_intrinsic: false,
94 name: rule.head.0.clone(),
95 }))
96 })
97 .collect();
98
99 let scc_detector = RuleCycleDetector {
100 possible_rules: rule_vertices,
101 edges: rule_edges,
102 };
103
104 let canonical_order: Vec<_> = scc_detector
106 .sccs()
107 .collect::<Vec<_>>()
108 .into_iter()
109 .rev()
110 .collect();
111
112 tracing::debug!(
113 "Canonical order: {}",
114 canonical_order
115 .iter()
116 .map(|nodes| nodes.iter().join(" <-> "))
117 .join(", ")
118 );
119
120 while !iteration_facts.is_empty() {
121 tracing::debug!(
123 "Iteration {counter}: {}",
124 iteration_facts
125 .iter()
126 .map(|i: &GroundedAtom| i.to_string())
127 .collect::<Vec<_>>()
128 .join(", ")
129 );
130 counter += 1;
131 total_facts.extend(iteration_facts.drain());
132
133 for rule_cycle in canonical_order.iter() {
134 let mut rule_queue: VecDeque<_> = rules
139 .iter()
140 .filter(|rule| {
141 rule_cycle
142 .iter()
143 .any(|head| head.to_string() == rule.head.0)
144 })
145 .collect();
146
147 while let Some(rule) = rule_queue.pop_front() {
148 tracing::info!("Expanding {rule}");
149 let pos: HashSet<_> = rule.body.iter().filter_map(BodyAtom::positive).collect();
151 let neg: HashSet<_> = rule.body.iter().filter_map(BodyAtom::negative).collect();
152
153 let rule_max = rule
154 .body
155 .iter()
156 .flat_map(|t| {
157 t.atom().terms.iter().filter_map(|t| match t {
158 Term::Variable(v) => Some(*v),
159 _ => None,
160 })
161 })
162 .max()
163 .unwrap_or(rule.mapping.len());
164
165 let mut mapping = HashMap::new();
166
167 for brule in rule.body.iter() {
168 let relevant: Vec<_> = total_facts
169 .iter()
170 .filter(|f| {
172 f.predicate == brule.atom().predicate
173 && f.terms.len() == brule.atom().terms.len()
174 })
175 .collect();
176 match brule {
177 BodyAtom::Negative(neg) => {
179 for fact in relevant.iter() {
180 for (fterm, term) in fact.terms.iter().zip(neg.terms.iter()) {
181 if let Term::Variable(v) = term {
182 let deny = mapping.entry(*v).or_insert(HashSet::new());
183 deny.insert(fterm.clone());
184 }
185 }
186 }
187 }
188 BodyAtom::Positive(_pos) => {
189 }
192 }
193 }
194
195 let inferred_mapping = (0..std::cmp::max(rule_max, rule.mapping.len()))
196 .map(|i| {
197 mapping
198 .get(&i)
199 .cloned()
200 .map(|deny| {
201 let undenied: HashSet<_> =
202 universe.difference(&deny).cloned().collect();
203
204 undenied
205 })
206 .unwrap_or(universe.iter().cloned().collect())
207 .into_iter()
208 .collect::<Vec<_>>()
209 })
210 .multi_cartesian_product()
211 .collect::<Vec<_>>();
212
213 if inferred_mapping.is_empty() {
215 let valid = pos
216 .iter()
217 .map(|ba| (ba, false))
218 .chain(neg.iter().map(|ba| (ba, true)))
219 .all(|(dep_ba, is_neg)| {
220 let Some(atom) = dep_ba.as_grounded() else {
221 return false;
223 };
224 if is_neg {
225 !total_facts.contains(&atom)
226 && !iteration_facts.contains(&atom)
227 && other
228 .iter()
229 .filter_map(|s| {
230 s.query(
231 &atom.predicate,
232 &atom.terms.iter().collect::<Vec<_>>(),
233 )
234 })
235 .all(|q| !q)
236 } else {
237 total_facts.contains(&atom)
238 || iteration_facts.contains(&atom)
239 || other
240 .iter()
241 .filter_map(|s| {
242 s.query(
243 &atom.predicate,
244 &atom.terms.iter().collect::<Vec<_>>(),
245 )
246 })
247 .any(|q| q)
248 }
249 });
250
251 if !valid {
252 continue;
253 }
254
255 let Some(fact) = rule
256 .head
257 .1
258 .iter()
259 .map(|t| t.as_grounded())
260 .collect::<Option<Vec<_>>>()
261 .map(|terms| GroundedAtom {
262 predicate: Predicate {
263 is_intrinsic: false,
264 name: rule.head.0.clone(),
265 },
266 terms,
267 }) else {
268 continue;
269 };
270
271 if !total_facts.contains(&fact) {
272 tracing::info!(
273 "Created static inference {}",
274 AtomDisplayWrapper(&Atom {
275 predicate: Predicate {
276 is_intrinsic: false,
277 name: rule.head.0.clone()
278 },
279 terms: rule.head.1.clone()
280 }),
281 );
282 iteration_facts.insert(fact.clone());
283 }
284 }
286
287 for inference in inferred_mapping.into_iter() {
290 let valid = pos
291 .iter()
292 .map(|ba| (ba, false))
293 .chain(neg.iter().map(|ba| (ba, true)))
294 .all(|(dep_ba, is_neg)| {
295 let atom = dep_ba.ground(&inference).unwrap();
296 if is_neg {
297 !total_facts.contains(&atom)
298 && !iteration_facts.contains(&atom)
299 && other
300 .iter()
301 .filter_map(|s| {
302 s.query(
303 &atom.predicate,
304 &atom.terms.iter().collect::<Vec<_>>(),
305 )
306 })
307 .all(|q| !q)
308 } else {
309 total_facts.contains(&atom)
310 || iteration_facts.contains(&atom)
311 || other
312 .iter()
313 .filter_map(|s| {
314 s.query(
315 &atom.predicate,
316 &atom.terms.iter().collect::<Vec<_>>(),
317 )
318 })
319 .any(|q| q)
320 }
321 });
322
323 if !valid {
324 continue;
325 }
326
327 let fact = rule
330 .head
331 .1
332 .iter()
333 .map(|t| t.ground(&inference))
334 .collect::<Option<Vec<_>>>()
335 .map(|terms| GroundedAtom {
336 predicate: Predicate {
337 is_intrinsic: false,
338 name: rule.head.0.clone(),
339 },
340 terms,
341 })
342 .unwrap();
343
344 if !total_facts.contains(&fact) {
345 tracing::info!(
346 "Created inference {} for {}",
347 AtomDisplayWrapper(&Atom {
348 predicate: Predicate {
349 is_intrinsic: false,
350 name: rule.head.0.clone()
351 },
352 terms: rule.head.1.clone()
353 }),
354 inference.iter().join(", ")
355 );
356 iteration_facts.insert(fact.clone());
357 }
358 }
359 }
360 }
361 }
362
363 (
364 Fixed {
365 facts: total_facts.into_iter().collect(),
366 },
367 universe,
368 )
369}
370
371#[derive(Debug)]
372#[cfg_attr(
373 feature = "serde_internal",
374 derive(serde::Serialize, serde::Deserialize)
375)]
376pub enum EvalOutput {
377 Proof(Option<Vec<GroundedBodyAtom>>),
381 Valid(HashMap<String, HashSet<GroundedTerm>>),
383 Invalid,
385}
386
387impl EvalOutput {
388 pub fn goal_satisfied(&self) -> bool {
389 match self {
390 Self::Proof(proof) => proof.is_some(),
392 Self::Valid(valid) => valid.iter().all(|(_, v)| !v.is_empty()),
395 Self::Invalid => false,
396 }
397 }
398}
399
400#[cfg_attr(feature = "web", wasm_bindgen)]
403#[cfg(feature = "web")]
404pub fn evaluate_program_nonasync_wasm(
405 program: JsValue,
406 storages: Box<[JsStorage]>,
407) -> Result<JsValue, JsValue> {
408 let program: CompiledProgram = serde_wasm_bindgen::from_value(program)?;
409 Ok(serde_wasm_bindgen::to_value(&evaluate_program_nonasync(
410 &program,
411 storages.iter().map(|i| i.erase()),
412 ))?)
413}
414
415#[cfg_attr(feature = "web", wasm_bindgen)]
416#[cfg(feature = "web")]
417pub fn compile_program_wasm(source: &str) -> Result<JsValue, JsError> {
418 Ok(serde_wasm_bindgen::to_value(&Compiler.compile(source)?)?)
419}
420
421#[cfg(feature = "async")]
422pub async fn evaluate_program_async<'a, I: IntoIterator<Item = ThreadsafeStorageRef<'a>>>(
423 program: &CompiledProgram,
424 storages: I,
425) -> EvalOutput {
426 if let Some(goal) = program.goal.as_grounded() {
427 eval_proof(program, goal, storages).await
429 } else {
430 let storages: Vec<_> = storages.into_iter().map(|s| s as &dyn Storage).collect();
431 let (_, variables) = eval_fixed_point(program, storages);
432 if variables.is_empty() {
433 EvalOutput::Invalid
434 } else {
435 EvalOutput::Valid(variables)
436 }
437 }
438}
439
440pub fn evaluate_program_nonasync<'a, I: IntoIterator<Item = StorageRef<'a>>>(
441 program: &CompiledProgram,
442 storages: I,
443) -> EvalOutput {
444 if let Some(goal) = program.goal.as_grounded() {
445 eval_grounded_fixed_point(goal, program, storages)
447 } else {
448 let storages: Vec<_> = storages.into_iter().map(|s| s as &dyn Storage).collect();
449 let (_, variables) = eval_fixed_point(program, storages);
450 if variables.is_empty() {
451 EvalOutput::Invalid
452 } else {
453 EvalOutput::Valid(variables)
454 }
455 }
456}
457
458#[cfg(feature = "async")]
460#[tracing::instrument(skip(facts, ext_storages))]
461fn provable_from_facts(
462 current_mapping: &[GroundedTerm],
463 facts: &[GroundedAtom],
464 subject: &Atom,
465 ext_storages: &[&(dyn Storage + Send + Sync)],
466) -> Option<(Vec<GroundedTerm>, Vec<GroundedBodyAtom>)> {
467 if let Some(ga) = subject.ground(current_mapping) {
469 if !ga.predicate.is_intrinsic && facts.contains(&ga) {
470 tracing::info!("Proved {}", ga);
471 Some((
472 current_mapping.to_vec(),
473 vec![GroundedBodyAtom::Positive(ga)],
474 ))
475 } else if ga.predicate.is_intrinsic {
476 let possible_storages: Vec<_> = ext_storages
477 .iter()
478 .filter_map(|s| s.query(&subject.predicate, &ga.terms.iter().collect::<Vec<_>>()))
479 .collect();
480 if possible_storages.is_empty() || possible_storages.into_iter().all(|q| !q) {
481 tracing::info!("Proved not {}", ga);
482 Some((
483 current_mapping.to_vec(),
484 vec![GroundedBodyAtom::Negative(ga)],
485 ))
486 } else {
487 tracing::info!("Proved {}", ga);
488 Some((
489 current_mapping.to_vec(),
490 vec![GroundedBodyAtom::Positive(ga)],
491 ))
492 }
493 } else {
494 tracing::info!("Could not prove {} from universe", ga);
495 None
496 }
497 } else {
498 tracing::info!("Failed to ground when reduced to universe");
501 None
502 }
503}
504
505#[cfg(feature = "async")]
506fn transitive_rewrite(target: &Atom, trial_mapping: &[GroundedTerm]) -> (Vec<GroundedTerm>, Atom) {
507 let mapping_shift = target
508 .terms
509 .iter()
510 .find_map(|t| match t {
511 Term::Variable(v) => Some(*v),
512 _ => None,
513 })
514 .unwrap_or(trial_mapping.len());
515
516 let rewrite_head = Atom {
517 predicate: target.predicate.clone(),
518 terms: target
519 .terms
520 .iter()
521 .cloned()
522 .map(|t| match t {
523 Term::Variable(v) => {
524 if v <= mapping_shift {
525 trial_mapping[v].clone().into()
526 } else {
527 Term::Variable(v - mapping_shift)
528 }
529 }
530 t => t,
531 })
532 .collect(),
533 };
534
535 let rotated = trial_mapping
536 .iter()
537 .skip(mapping_shift)
538 .take(target.terms.len())
539 .cloned()
540 .collect();
541
542 tracing::warn!(
543 "transitive -> {} {trial_mapping:?} {} {rotated:?}",
544 AtomDisplayWrapper(target),
545 AtomDisplayWrapper(&rewrite_head),
546 );
547
548 (rotated, rewrite_head)
549}
550
551#[cfg(feature = "async")]
552#[async_recursion::async_recursion]
553#[tracing::instrument(skip(universe, facts, rules, ext_storages))]
554async fn provable(
555 universe: &[GroundedTerm],
556 current_mapping: &[GroundedTerm],
557 facts: &[GroundedAtom],
558 rules: &[Rule],
559 subject: &Atom,
560 ext_storages: &[&(dyn Storage + Send + Sync)],
561) -> Option<(Vec<GroundedTerm>, Vec<GroundedBodyAtom>)> {
562 let possible: Vec<_> = rules
565 .iter()
566 .filter(|rule| {
567 rule.head.0 == subject.predicate.to_string() && rule.head.1.len() == subject.terms.len()
568 })
569 .collect();
570
571 if let Some((mapping, proof)) =
573 provable_from_facts(current_mapping, facts, subject, ext_storages)
574 {
575 return Some((mapping, proof));
576 } else {
577 'ruleexp: for rule in possible.iter() {
579 let rule_head = Atom {
580 predicate: subject.predicate.clone(),
581 terms: rule.head.1.clone(),
582 };
583
584 tracing::trace!("Found relevant rule: {rule}");
585
586 let head_vars = rule
587 .head
588 .1
589 .iter()
590 .filter_map(|t| match t {
591 Term::Variable(v) => Some(*v),
592 _ => None,
593 })
594 .collect::<HashSet<_>>();
595
596 let body_vars = rule
597 .body
598 .iter()
599 .flat_map(|ba| ba.atom().terms.iter())
600 .filter_map(|t| match t {
601 Term::Variable(v) => Some(*v),
602 _ => None,
603 })
604 .collect::<HashSet<_>>();
605
606 tracing::warn!("Detected vars from head {head_vars:?}, body {body_vars:?}",);
607
608 let excluded_rules: Vec<_> = rules
609 .iter()
610 .cloned()
611 .filter(|frule| !(frule.head == rule.head && frule.body == rule.body))
612 .collect();
613
614 let var_offset = body_vars.difference(&head_vars).count();
615 let first_var = rule_head
616 .terms
617 .iter()
618 .position(|t| matches!(t, Term::Variable(_)))
619 .unwrap_or(rule_head.terms.len());
620
621 tracing::debug!("{} {current_mapping:?}", AtomDisplayWrapper(subject));
622
623 let nvars: Vec<_> = rule_head
624 .terms
625 .iter()
626 .positions(|t| !matches!(t, Term::Variable(_)))
627 .collect();
628
629 let original_mapping = ¤t_mapping;
630 let current_mapping: Vec<_> = subject
631 .terms
632 .iter()
633 .filter_map(|t| match t {
634 Term::Variable(v) => Some(*v),
635 _ => None,
636 })
637 .map(|v| current_mapping[v].clone())
638 .enumerate()
639 .filter(|(i, _)| !nvars.contains(i))
640 .map(|(_, v)| v)
641 .collect();
642
643 tracing::debug!("New {current_mapping:?} {nvars:?}");
644
645 if body_vars.len() > current_mapping.len() {
647 let body_mappings = (0..body_vars.len() - current_mapping.len())
648 .map(|_| universe.iter().cloned())
649 .multi_cartesian_product();
650
651 'mapexp: for body_mapping in body_mappings {
652 let mut grounded_proof = vec![];
653
654 tracing::warn!(
655 "Using body mapping {} (cmapping {:?}) (with voffset {}) ",
656 body_mapping.iter().join(", "),
657 current_mapping,
658 var_offset,
659 );
660
661 let composite_mapping: Vec<_> = current_mapping
662 .iter()
663 .chain(body_mapping.iter())
664 .cloned()
665 .collect();
666
667 for ba in rule.body.iter() {
668 match ba {
669 BodyAtom::Positive(atom) => {
670 let (rotated, rewrite_head) =
671 transitive_rewrite(&atom, &composite_mapping);
672
673 if let Some((_mapping, proof)) = provable(
675 universe,
676 &rotated,
677 facts,
678 &excluded_rules,
679 &rewrite_head,
680 ext_storages,
681 )
682 .await
683 {
684 if matches!(proof[0], GroundedBodyAtom::Positive(_)) {
686 grounded_proof.extend(proof.into_iter());
687 } else {
688 continue 'mapexp;
689 }
690 } else {
691 continue 'mapexp;
692 }
693 }
694 BodyAtom::Negative(atom) => {
695 let (rotated, rewrite_head) =
696 transitive_rewrite(&atom, &composite_mapping);
697
698 if let Some((_mapping, proof)) = provable(
699 universe,
700 &rotated, facts,
702 &excluded_rules,
703 &rewrite_head,
704 ext_storages,
705 )
706 .await
707 {
708 if matches!(proof[0], GroundedBodyAtom::Negative(_)) {
709 grounded_proof.extend(proof.into_iter());
710 } else {
711 continue 'mapexp;
712 }
713 } else {
714 continue;
715 }
716 }
717 }
718 }
719
720 let subjected_mapping: Vec<_> = composite_mapping
721 .iter()
722 .skip(var_offset)
724 .take(head_vars.len())
725 .cloned()
726 .collect();
727
728 tracing::info!(
729 "Found body proof for: {} from {} with mapping {:?} (current {:?}) {var_offset} s {first_var} {:?} {:?}",
730 AtomDisplayWrapper(&subject),
731 AtomDisplayWrapper(&rule_head),
732 composite_mapping,
733 current_mapping,
734 original_mapping,
735 subjected_mapping,
736 );
737
738 let mapped_rule = rule_head.ground(&composite_mapping);
739 let mapped_subject = subject.ground(&original_mapping);
740
741 tracing::info!(
742 "Comparing body {} to {}",
743 match &mapped_rule {
744 Some(rule) => format!("{}", rule),
745 None => "no rule".to_string(),
746 },
747 match &mapped_subject {
748 Some(rule) => format!("{}", rule),
749 None => "no rule".to_string(),
750 }
751 );
752
753 if let (Some(mapped_rule), Some(mapped_subject)) = (mapped_rule, mapped_subject)
754 {
755 let new_proof = std::iter::once(GroundedBodyAtom::Positive(
756 if mapped_subject != mapped_rule {
757 GroundedAtom {
758 predicate: subject.predicate.clone(),
759 terms: mapped_rule
760 .terms
761 .iter()
762 .take(first_var)
763 .chain(original_mapping.iter())
764 .take(subject.terms.len())
765 .cloned()
766 .collect(),
767 }
768 } else {
769 mapped_subject.clone()
770 },
771 ))
772 .chain(grounded_proof.into_iter())
773 .collect();
774
775 tracing::info!(
776 "{:?} {:?} {:?} {:?} <- IOU {} {var_offset}",
777 &composite_mapping,
778 &mapped_subject.terms,
779 body_mapping,
780 &original_mapping,
781 AtomDisplayWrapper(subject),
782 );
783 if mapped_subject != mapped_rule {
784 continue 'mapexp;
785 }
786 return Some((composite_mapping, new_proof));
787 } else {
788 continue 'mapexp;
789 }
790 }
791 } else {
792 tracing::warn!(
793 "Rule fully grounded, using mapping {} ({var_offset})",
794 current_mapping.iter().join(", ")
795 );
796
797 let mut grounded_proof = vec![];
798
799 let direct_mapping: Vec<_> = current_mapping.clone();
800
801 tracing::warn!(
802 "Using direct mapping {:?} {:?} {:?}",
803 direct_mapping,
804 current_mapping,
805 original_mapping
806 );
807
808 for ba in rule.body.iter() {
809 match ba {
810 BodyAtom::Positive(atom) => {
811 if let Some((_mapping, proof)) = provable(
813 universe,
814 &direct_mapping, facts,
816 &excluded_rules,
817 &atom,
818 ext_storages,
819 )
820 .await
821 {
822 if matches!(proof[0], GroundedBodyAtom::Positive(_)) {
823 grounded_proof.extend(proof.into_iter());
824 } else {
825 continue 'ruleexp;
826 }
827 } else {
828 continue 'ruleexp;
829 }
830 }
831 BodyAtom::Negative(atom) => {
832 if let Some((_mapping, proof)) = provable(
833 universe,
834 &direct_mapping,
835 facts,
836 &excluded_rules,
837 &atom,
838 ext_storages,
839 )
840 .await
841 {
842 if matches!(proof[0], GroundedBodyAtom::Negative(_)) {
843 grounded_proof.extend(proof.into_iter());
844 } else {
845 continue 'ruleexp;
846 }
847 } else {
848 continue;
849 }
850 }
851 }
852 }
853
854 tracing::info!(
855 "Found proof for: {} from {} with mapping {:?} direct {:?}",
856 AtomDisplayWrapper(subject),
857 AtomDisplayWrapper(&rule_head),
858 current_mapping,
859 direct_mapping,
860 );
861
862 let mapped_subject = subject.ground(&original_mapping).unwrap();
863 let mapped_rule = rule_head.ground(&direct_mapping).unwrap();
864 let new_proof = std::iter::once(GroundedBodyAtom::Positive(mapped_rule.clone()))
865 .chain(grounded_proof.into_iter())
866 .collect();
867
868 if mapped_rule != mapped_subject {
869 continue 'ruleexp;
870 }
871
872 return Some((current_mapping.to_vec(), new_proof));
873 }
874 }
875
876 tracing::info!(
877 "Failed to prove subject {} for {}",
878 AtomDisplayWrapper(subject),
879 current_mapping.iter().join(", ")
880 );
881
882 subject.ground(current_mapping).map(|mapped_subject| {
883 (
884 current_mapping.to_vec(),
885 vec![GroundedBodyAtom::Negative(mapped_subject)],
886 )
887 })
888 }
889}
890
891#[cfg(feature = "async")]
892async fn eval_proof<'a, I: IntoIterator<Item = ThreadsafeStorageRef<'a>>>(
893 program: &CompiledProgram,
894 goal: GroundedGoal,
895 storages: I,
896) -> EvalOutput {
897 let ext_storages: Arc<Vec<_>> = Arc::new(storages.into_iter().collect());
898 let universe: HashSet<_> = program.universe().into_iter().collect();
899 let storage_universe: HashSet<_> = ext_storages
900 .iter()
901 .flat_map(|s| {
902 s.as_fixed()
903 .map(|f| f.get_facts().into_iter().flat_map(|a| a.terms).collect())
904 .unwrap_or(vec![])
905 .into_iter()
906 })
907 .collect();
908 let total_universe: Vec<_> = universe.union(&storage_universe).cloned().collect();
909
910 let handles: Vec<_> = goal
911 .components
912 .clone()
913 .into_iter()
914 .map(|comp| {
915 let ext_storages = ext_storages.clone();
916 let base_mapping = comp.atom().terms.clone();
917 let facts = program
918 .facts
919 .iter()
920 .map(|(name, terms)| GroundedAtom {
921 predicate: Predicate {
922 is_intrinsic: false,
923 name: name.clone(),
924 },
925 terms: terms.clone(),
926 })
927 .chain(
928 ext_storages
929 .iter()
930 .filter_map(|s| s.as_fixed())
931 .flat_map(|fs| fs.get_facts()),
932 )
933 .collect::<Vec<_>>();
934 let is_neg = matches!(comp, GroundedBodyAtom::Negative(_));
935 let subject = Atom {
936 predicate: comp.atom().predicate.clone(),
937 terms: (0..comp.atom().terms.len()).map(Term::Variable).collect(),
945 };
946 let rules = program.rules.clone();
947 let universe = total_universe.clone();
948 async move {
949 (
950 subject.clone(),
951 provable(
952 &universe,
953 &base_mapping,
955 &facts,
956 &rules,
957 &subject,
958 &ext_storages,
959 )
960 .await,
961 is_neg,
962 )
963 }
964 })
965 .collect();
966
967 let handle_future = futures::future::join_all(handles);
968
969 let subtrees = handle_future.await.into_iter().collect::<Vec<_>>();
970
971 let overall_proof: Option<Vec<GroundedBodyAtom>> =
972 subtrees.into_iter().try_fold(vec![], |otree, proof| {
973 if let (subj, Some((_mapping, proof)), false) = proof {
974 tracing::warn!("Component proof: {}", proof.iter().join(" <- "));
975 if matches!(proof[0], GroundedBodyAtom::Positive(_))
976 && proof.iter().any(|pa| {
977 pa.atom().predicate == subj.predicate
978 && matches!(pa, GroundedBodyAtom::Negative(_))
979 })
980 || matches!(proof[0], GroundedBodyAtom::Negative(_))
981 && (proof.iter().any(|pa| {
982 pa.atom().predicate == subj.predicate
983 && matches!(pa, GroundedBodyAtom::Positive(_))
984 }) || proof.len() == 1)
985 {
986 None
987 } else {
988 Some(otree.into_iter().chain(proof.into_iter()).collect())
989 }
990 } else if let (subj, Some((_mapping, proof)), true) = proof {
991 tracing::warn!("Negative component proof: {}", proof.iter().join(" <- "));
992 if proof.is_empty() {
993 Some(otree)
994 } else if (matches!(proof[0], GroundedBodyAtom::Positive(_))
995 && !proof.iter().any(|pa| {
996 pa.atom().predicate == subj.predicate
997 && matches!(pa, GroundedBodyAtom::Negative(_))
998 }))
999 {
1000 None
1001 } else {
1002 Some(otree.into_iter().chain(proof.into_iter()).collect())
1003 }
1004 } else if let (_, None, true) = proof {
1005 Some(otree)
1006 } else {
1007 None
1008 }
1009 });
1010
1011 EvalOutput::Proof(overall_proof)
1012}
1013
1014fn eval_grounded_fixed_point<'a, I: IntoIterator<Item = &'a dyn Storage>>(
1015 goal: GroundedGoal,
1016 program: &CompiledProgram,
1017 ext_storages: I,
1018) -> EvalOutput {
1019 let mut proof = vec![];
1028
1029 let ext_storages: Vec<_> = ext_storages.into_iter().collect();
1030 let (program_storage, _results) = eval_fixed_point(program, ext_storages.iter().copied());
1031
1032 tracing::info!(
1033 "fixed-point storage: {}",
1034 program_storage
1035 .facts
1036 .iter()
1037 .map(|f| format!("{f}."))
1038 .join("")
1039 );
1040
1041 for component in goal.components {
1043 match &component {
1044 GroundedBodyAtom::Negative(neg) => {
1045 if program_storage.query(&neg.predicate, &neg.terms.iter().collect::<Vec<_>>())
1046 == Some(true)
1047 || ext_storages
1048 .iter()
1049 .filter_map(|s| {
1050 s.query(&neg.predicate, &neg.terms.iter().collect::<Vec<_>>())
1051 })
1052 .any(|q| q)
1053 {
1054 return EvalOutput::Invalid;
1055 } else {
1056 proof.push(component.clone());
1057 }
1058 }
1059 GroundedBodyAtom::Positive(pos) => {
1060 if program_storage.query(&pos.predicate, &pos.terms.iter().collect::<Vec<_>>())
1061 != Some(true)
1062 && !ext_storages
1063 .iter()
1064 .filter_map(|s| {
1065 s.query(&pos.predicate, &pos.terms.iter().collect::<Vec<_>>())
1066 })
1067 .any(|q| q)
1068 {
1069 return EvalOutput::Invalid;
1070 } else {
1071 proof.push(component.clone());
1072 }
1073 }
1074 }
1075 }
1076 EvalOutput::Proof(Some(proof))
1077}
1078
1079fn eval_fixed_point<'a, I: IntoIterator<Item = &'a dyn Storage>>(
1080 program: &CompiledProgram,
1081 storages: I,
1082) -> (Fixed, HashMap<String, HashSet<GroundedTerm>>) {
1083 let mut satisfactory_values: HashMap<String, HashSet<GroundedTerm>> = HashMap::new();
1085 let ext_storages: Vec<_> = storages.into_iter().collect();
1086 let universe = program.universe();
1087
1088 let (program_storage, universe) =
1090 fixed_point_expand(&program.facts, &program.rules, &universe, &ext_storages);
1091
1092 {
1093 tracing::debug!("Program Proved:\n{program_storage}");
1094
1095 'scan: for variable_set in [&universe]
1096 .into_iter()
1097 .map(|i| i.iter().cloned().collect::<Vec<_>>())
1098 .cycle()
1099 .take(program.goal.mapping.len())
1100 .multi_cartesian_product()
1101 {
1102 for goal_component in program.goal.components.iter() {
1103 let (atom, neg) = (
1104 goal_component.atom(),
1105 matches!(goal_component, BodyAtom::Negative(_)),
1106 );
1107
1108 let gatom = atom.ground(&variable_set.to_vec()).unwrap();
1109
1110 let proved = program_storage
1113 .query(&gatom.predicate, &gatom.terms.iter().collect::<Vec<_>>())
1114 .unwrap_or(false);
1115
1116 let found = proved
1117 || ext_storages
1118 .iter()
1119 .filter_map(|s| {
1120 s.query(&gatom.predicate, &gatom.terms.iter().collect::<Vec<_>>())
1121 })
1122 .any(|q| q);
1123
1124 tracing::trace!("For vars {variable_set:?}: {gatom} {found} {neg}");
1125
1126 if neg {
1127 if found {
1128 continue 'scan;
1129 }
1130 } else if !found {
1131 continue 'scan;
1132 }
1133 }
1134
1135 for (idx, variable) in variable_set.into_iter().enumerate() {
1136 satisfactory_values
1137 .entry(program.goal.mapping[idx].clone())
1138 .or_insert(HashSet::new())
1139 .insert(variable.clone());
1140 }
1141 }
1142 }
1143
1144 (program_storage, satisfactory_values)
1145}
1146
1147#[cfg(test)]
1148mod tests {
1149 use crate::{library::StandardLibrary, StorageRef, ThreadsafeStorageRef};
1150
1151 use super::{evaluate_program_async, evaluate_program_nonasync, Compiler};
1152 use datadriven::walk;
1153 use tokio::runtime::Builder;
1154
1155 #[test]
1156 fn run() {
1157 walk("tests/eval", |f| {
1158 f.run(|test| -> String {
1159 let test_storages = vec![StandardLibrary];
1160 match test.directive.as_str() {
1161 "async" => {
1162 let rt = Builder::new_multi_thread().build().unwrap();
1163 match Compiler.compile(&test.input) {
1164 Ok(program) => format!(
1165 "{}",
1166 match rt.block_on(evaluate_program_async(
1167 &program,
1168 test_storages.iter().map(|s| s as ThreadsafeStorageRef)
1169 )) {
1170 crate::EvalOutput::Invalid => "INVALID".to_string(),
1171 crate::EvalOutput::Proof(proof) => match proof {
1172 Some(proof) => proof
1173 .iter()
1174 .map(|i| i.to_string())
1175 .collect::<Vec<_>>()
1176 .join(" -> "),
1177 None => "NO PROOF".to_string(),
1178 },
1179 crate::EvalOutput::Valid(mapping) => mapping
1180 .into_iter()
1181 .map(|(k, vs)| format!(
1182 "{k}: {}",
1183 vs.into_iter()
1184 .map(|i| i.to_string())
1185 .collect::<Vec<_>>()
1186 .join(", ")
1187 ))
1188 .collect::<Vec<_>>()
1189 .join("\n"),
1190 }
1191 ),
1192 Err(e) => format!("Error: {e}"),
1193 }
1194 }
1195 "sync" => match Compiler.compile(&test.input) {
1196 Ok(program) => {
1197 format!(
1198 "{}",
1199 match evaluate_program_nonasync(
1200 &program,
1201 test_storages.iter().map(|s| s as StorageRef)
1202 ) {
1203 crate::EvalOutput::Invalid => "INVALID".to_string(),
1204 crate::EvalOutput::Proof(proof) => match proof {
1205 Some(proof) => proof
1206 .iter()
1207 .map(|i| i.to_string())
1208 .collect::<Vec<_>>()
1209 .join(" -> "),
1210 None => "NO PROOF".to_string(),
1211 },
1212 crate::EvalOutput::Valid(mapping) => mapping
1213 .into_iter()
1214 .map(|(k, vs)| format!(
1215 "{k}: {}",
1216 vs.into_iter()
1217 .map(|i| i.to_string())
1218 .collect::<Vec<_>>()
1219 .join(", ")
1220 ))
1221 .collect::<Vec<_>>()
1222 .join("\n"),
1223 }
1224 )
1225 }
1226 Err(e) => format!("Error: {e}"),
1227 },
1228 _ => "Invalid directive".to_string(),
1229 }
1230 })
1231 });
1232 }
1233}