arde/
lib.rs

1//! Our goal: be able to store selection queries in simple one-liners.
2//!
3//! We will model it after logic programming.
4
5use 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    // Try to do semi-naïve evaluation
40    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    // Get the actual universe (including terms from storage assertions)
59    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    // Regenerate rule edges
70    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    // How we generate the canonical order to visit rules to generate facts
105    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        // Learn all the current iteration's facts
122        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 analyzed: HashSet<_> = HashSet::new();
135            // let mut executed_count = 0;
136
137            // let mut done_expanding = HashSet::new();
138            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                // All rule terms are groundable
150                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                        // .chain(iteration_facts.iter())
171                        .filter(|f| {
172                            f.predicate == brule.atom().predicate
173                                && f.terms.len() == brule.atom().terms.len()
174                        })
175                        .collect();
176                    match brule {
177                        // Negative atoms cannot tell us values a variable can take, but can restrict the universe
178                        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                            // Dont check positive values, we don't need to as we test
190                            // all combinations of the universe
191                        }
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 0, the fact must be proven w/o input variables
214                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                                // Rule is not valid if it is ungroundable
222                                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                    // continue;
285                }
286
287                // Now we are only exploring a fully body-groundable rule, where all body atoms are can be known
288
289                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                    // mutated = true;
328
329                    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 tree for the goal.
378    /// Returned if the goal is grounded.
379    /// None indicates ungrounded goals exist.
380    Proof(Option<Vec<GroundedBodyAtom>>),
381    /// Sets of valid values that a term can take to satisfy the program.
382    Valid(HashMap<String, HashSet<GroundedTerm>>),
383    /// No variables were groundable.
384    Invalid,
385}
386
387impl EvalOutput {
388    pub fn goal_satisfied(&self) -> bool {
389        match self {
390            // A proof exists.
391            Self::Proof(proof) => proof.is_some(),
392            // All variables must have a value that they solve for
393            // (which we encode using the Result)
394            Self::Valid(valid) => valid.iter().all(|(_, v)| !v.is_empty()),
395            Self::Invalid => false,
396        }
397    }
398}
399
400// No async web, b/c JS values are !Send + !Sync
401
402#[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        // Proof (top-down) semantics
428        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        // Proof (top-down) semantics
446        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// Returns the mapping to use and the current proof
459#[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    // Can the head be grounded (instatiated) with the mapping given?
468    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        // Current mapping has some unbounded variables
499        // We cannot prove something we do not know, fail the proof.
500        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    // If we have an unbound variable, search for in-program facts that match the shape
563    // try to prove it.
564    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    // let finalized_mapping: Vec<_> = current_mapping.iter().rev().cloned().collect();
572    if let Some((mapping, proof)) =
573        provable_from_facts(current_mapping, facts, subject, ext_storages)
574    {
575        return Some((mapping, proof));
576    } else {
577        // Try to find a possible mapping for subrules
578        '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 = &current_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            // Variables might show in body, but not in head!!!
646            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                                // Find a proof for atom, if not, fail proof
674                                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                                    // let grounded_head = atom.ground(&)
685                                    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, //mapping_shift + vc],
701                                    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                        // .cycle()
723                        .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                            // Find a proof for atom, if not, fail proof
812                            if let Some((_mapping, proof)) = provable(
813                                universe,
814                                &direct_mapping, //[mapping_shift..],
815                                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: comp
938                //     .atom()
939                //     .terms
940                //     .iter()
941                //     .cloned()
942                //     .map(|i| i.into())
943                //     .collect(),
944                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                        // &[],
954                        &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    // TODO Come up w/ faster way of proving (not rule(X) and other_rule(X))
1020    // TODO I might have an idea of how to do this, but I'd need some time to implement
1021
1022    // Oof, I could ask fact(X), not fact2(Y), and it should be able to tell me
1023    // X and Y such that they are both true...
1024    // It should be even able to tell me what makes not fact2(Y) true by itself.
1025    // Where X and Y are real terms that could also be proven by an intrinsic...
1026
1027    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    // Not every variable was resolved.
1042    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    // Fixed-point semantics
1084    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    // Ignore the output universe (which contains synthesized values)
1089    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                // Do we need to ask external storage?
1111
1112                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}