1use crate::cache::{CacheManager, QueryKey};
12use crate::ir::{KnowledgeBase, Predicate, Rule, Term};
13use ipfrs_core::error::Result;
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17
18pub type Substitution = HashMap<String, Term>;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct Proof {
24 pub goal: Predicate,
26 pub rule: Option<ProofRule>,
28 pub subproofs: Vec<Proof>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ProofRule {
35 pub head: Predicate,
37 pub body: Vec<Predicate>,
39 pub is_fact: bool,
41}
42
43impl Proof {
44 pub fn fact(goal: Predicate) -> Self {
46 Self {
47 rule: Some(ProofRule {
48 head: goal.clone(),
49 body: Vec::new(),
50 is_fact: true,
51 }),
52 goal,
53 subproofs: Vec::new(),
54 }
55 }
56
57 pub fn from_rule(goal: Predicate, rule: &Rule, subproofs: Vec<Proof>) -> Self {
59 Self {
60 goal,
61 rule: Some(ProofRule {
62 head: rule.head.clone(),
63 body: rule.body.clone(),
64 is_fact: false,
65 }),
66 subproofs,
67 }
68 }
69
70 pub fn depth(&self) -> usize {
72 if self.subproofs.is_empty() {
73 1
74 } else {
75 1 + self.subproofs.iter().map(|p| p.depth()).max().unwrap_or(0)
76 }
77 }
78
79 #[inline]
81 pub fn size(&self) -> usize {
82 1 + self.subproofs.iter().map(|p| p.size()).sum::<usize>()
83 }
84
85 #[inline]
87 pub fn is_fact(&self) -> bool {
88 self.subproofs.is_empty()
89 }
90
91 pub fn all_goals(&self) -> Vec<&Predicate> {
93 let mut goals = vec![&self.goal];
94 for subproof in &self.subproofs {
95 goals.extend(subproof.all_goals());
96 }
97 goals
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct GoalDecomposition {
104 pub goal: Predicate,
106 pub subgoals: Vec<Predicate>,
108 pub rule_applied: Option<Rule>,
110 pub local_solutions: Vec<bool>,
112 pub depth: usize,
114}
115
116impl GoalDecomposition {
117 pub fn new(goal: Predicate, depth: usize) -> Self {
119 Self {
120 goal,
121 subgoals: Vec::new(),
122 rule_applied: None,
123 local_solutions: Vec::new(),
124 depth,
125 }
126 }
127
128 pub fn apply_rule(&mut self, rule: &Rule) {
130 self.rule_applied = Some(rule.clone());
131 self.subgoals = rule.body.clone();
132 self.local_solutions = vec![false; rule.body.len()];
133 }
134
135 pub fn mark_solved(&mut self, index: usize) {
137 if index < self.local_solutions.len() {
138 self.local_solutions[index] = true;
139 }
140 }
141
142 #[inline]
144 pub fn is_complete(&self) -> bool {
145 self.local_solutions.iter().all(|&solved| solved)
146 }
147
148 pub fn unsolved_subgoals(&self) -> Vec<&Predicate> {
150 self.subgoals
151 .iter()
152 .zip(self.local_solutions.iter())
153 .filter(|(_, &solved)| !solved)
154 .map(|(sg, _)| sg)
155 .collect()
156 }
157}
158
159#[derive(Debug, Clone, Default)]
161pub struct CycleDetector {
162 goal_stack: Vec<String>,
164 goal_set: HashSet<String>,
166}
167
168impl CycleDetector {
169 pub fn new() -> Self {
171 Self::default()
172 }
173
174 #[inline]
176 pub fn push(&mut self, goal: &Predicate) -> bool {
177 let key = goal_to_key(goal);
178 if self.goal_set.contains(&key) {
179 return false; }
181 self.goal_set.insert(key.clone());
182 self.goal_stack.push(key);
183 true
184 }
185
186 #[inline]
188 pub fn pop(&mut self) {
189 if let Some(key) = self.goal_stack.pop() {
190 self.goal_set.remove(&key);
191 }
192 }
193
194 #[inline]
196 pub fn would_cycle(&self, goal: &Predicate) -> bool {
197 let key = goal_to_key(goal);
198 self.goal_set.contains(&key)
199 }
200
201 #[inline]
203 pub fn depth(&self) -> usize {
204 self.goal_stack.len()
205 }
206
207 pub fn clear(&mut self) {
209 self.goal_stack.clear();
210 self.goal_set.clear();
211 }
212}
213
214fn goal_to_key(goal: &Predicate) -> String {
216 format!("{}({})", goal.name, goal.args.len())
217}
218
219#[derive(Default)]
221pub struct InferenceEngine {
222 max_depth: usize,
224 max_solutions: usize,
226 cycle_detection: bool,
228}
229
230impl InferenceEngine {
231 #[inline]
233 pub fn new() -> Self {
234 Self {
235 max_depth: 100,
236 max_solutions: 100,
237 cycle_detection: true,
238 }
239 }
240
241 #[inline]
243 pub fn with_limits(max_depth: usize, max_solutions: usize) -> Self {
244 Self {
245 max_depth,
246 max_solutions,
247 cycle_detection: true,
248 }
249 }
250
251 #[inline]
253 pub fn with_cycle_detection(mut self, enabled: bool) -> Self {
254 self.cycle_detection = enabled;
255 self
256 }
257
258 pub fn query(&self, goal: &Predicate, kb: &KnowledgeBase) -> Result<Vec<Substitution>> {
260 let mut solutions = Vec::new();
261 let initial_subst = Substitution::new();
262
263 self.solve_goal(goal, &initial_subst, kb, 0, &mut solutions)?;
264
265 Ok(solutions)
266 }
267
268 pub fn prove(&self, goal: &Predicate, kb: &KnowledgeBase) -> Result<Option<Proof>> {
270 let initial_subst = Substitution::new();
271 self.prove_goal(goal, &initial_subst, kb, 0)
272 }
273
274 fn solve_goal(
276 &self,
277 goal: &Predicate,
278 subst: &Substitution,
279 kb: &KnowledgeBase,
280 depth: usize,
281 solutions: &mut Vec<Substitution>,
282 ) -> Result<()> {
283 if depth > self.max_depth {
285 return Ok(());
286 }
287
288 if solutions.len() >= self.max_solutions {
290 return Ok(());
291 }
292
293 let goal = apply_subst_predicate(goal, subst);
295
296 for fact in kb.get_predicates(&goal.name) {
298 if let Some(new_subst) = unify_predicates(&goal, fact, subst) {
299 solutions.push(new_subst);
300 if solutions.len() >= self.max_solutions {
301 return Ok(());
302 }
303 }
304 }
305
306 for rule in kb.get_rules(&goal.name) {
308 let renamed_rule = rename_rule_vars(rule, depth);
310
311 if let Some(new_subst) = unify_predicates(&goal, &renamed_rule.head, subst) {
313 self.solve_conjunction(&renamed_rule.body, &new_subst, kb, depth + 1, solutions)?;
315 }
316 }
317
318 Ok(())
319 }
320
321 fn solve_conjunction(
323 &self,
324 goals: &[Predicate],
325 subst: &Substitution,
326 kb: &KnowledgeBase,
327 depth: usize,
328 solutions: &mut Vec<Substitution>,
329 ) -> Result<()> {
330 if goals.is_empty() {
331 solutions.push(subst.clone());
333 return Ok(());
334 }
335
336 let first_goal = &goals[0];
338 let rest_goals = &goals[1..];
339
340 let mut intermediate_solutions = Vec::new();
341 self.solve_goal(first_goal, subst, kb, depth, &mut intermediate_solutions)?;
342
343 for intermediate_subst in intermediate_solutions {
345 if solutions.len() >= self.max_solutions {
346 return Ok(());
347 }
348 self.solve_conjunction(rest_goals, &intermediate_subst, kb, depth, solutions)?;
349 }
350
351 Ok(())
352 }
353
354 fn prove_goal(
356 &self,
357 goal: &Predicate,
358 subst: &Substitution,
359 kb: &KnowledgeBase,
360 depth: usize,
361 ) -> Result<Option<Proof>> {
362 if depth > self.max_depth {
364 return Ok(None);
365 }
366
367 let goal = apply_subst_predicate(goal, subst);
369
370 for fact in kb.get_predicates(&goal.name) {
372 if let Some(_new_subst) = unify_predicates(&goal, fact, subst) {
373 return Ok(Some(Proof::fact(goal)));
374 }
375 }
376
377 for rule in kb.get_rules(&goal.name) {
379 let renamed_rule = rename_rule_vars(rule, depth);
381
382 if let Some(new_subst) = unify_predicates(&goal, &renamed_rule.head, subst) {
384 if let Some(subproofs) =
386 self.prove_conjunction(&renamed_rule.body, &new_subst, kb, depth + 1)?
387 {
388 return Ok(Some(Proof::from_rule(goal, &renamed_rule, subproofs)));
389 }
390 }
391 }
392
393 Ok(None)
394 }
395
396 fn prove_conjunction(
398 &self,
399 goals: &[Predicate],
400 subst: &Substitution,
401 kb: &KnowledgeBase,
402 depth: usize,
403 ) -> Result<Option<Vec<Proof>>> {
404 if goals.is_empty() {
405 return Ok(Some(Vec::new()));
406 }
407
408 let first_goal = &goals[0];
409 let rest_goals = &goals[1..];
410
411 if let Some(first_proof) = self.prove_goal(first_goal, subst, kb, depth)? {
413 if let Some(rest_proofs) = self.prove_conjunction(rest_goals, subst, kb, depth)? {
415 let mut all_proofs = vec![first_proof];
416 all_proofs.extend(rest_proofs);
417 return Ok(Some(all_proofs));
418 }
419 }
420
421 Ok(None)
422 }
423
424 pub fn verify(&self, proof: &Proof, kb: &KnowledgeBase) -> Result<bool> {
426 self.verify_proof_recursive(proof, kb, 0)
427 }
428
429 fn verify_proof_recursive(
431 &self,
432 proof: &Proof,
433 kb: &KnowledgeBase,
434 depth: usize,
435 ) -> Result<bool> {
436 if depth > self.max_depth {
438 return Ok(false);
439 }
440
441 let Some(ref rule) = proof.rule else {
443 return Ok(false);
444 };
445
446 if rule.is_fact {
448 let facts = kb.get_predicates(&proof.goal.name);
450 for fact in facts {
451 if unify_predicates(&proof.goal, fact, &Substitution::new()).is_some() {
452 return Ok(true);
453 }
454 }
455 return Ok(false);
456 }
457
458 let rules = kb.get_rules(&proof.goal.name);
461 let mut rule_exists = false;
462 for kb_rule in rules {
463 if kb_rule.head.name == rule.head.name
465 && kb_rule.head.args.len() == rule.head.args.len()
466 && kb_rule.body.len() == rule.body.len()
467 {
468 let bodies_match = kb_rule
470 .body
471 .iter()
472 .zip(rule.body.iter())
473 .all(|(b1, b2)| b1.name == b2.name && b1.args.len() == b2.args.len());
474
475 if bodies_match {
476 rule_exists = true;
477 break;
478 }
479 }
480 }
481
482 if !rule_exists {
483 return Ok(false);
484 }
485
486 if proof.subproofs.len() != rule.body.len() {
488 return Ok(false);
489 }
490
491 for (i, subproof) in proof.subproofs.iter().enumerate() {
493 let body_predicate = &rule.body[i];
495 if subproof.goal.name != body_predicate.name {
496 return Ok(false);
497 }
498
499 if !self.verify_proof_recursive(subproof, kb, depth + 1)? {
501 return Ok(false);
502 }
503 }
504
505 Ok(true)
507 }
508}
509
510pub fn unify(t1: &Term, t2: &Term, subst: &Substitution) -> Option<Substitution> {
512 let t1 = apply_subst_term(t1, subst);
513 let t2 = apply_subst_term(t2, subst);
514
515 match (&t1, &t2) {
516 (Term::Const(c1), Term::Const(c2)) if c1 == c2 => Some(subst.clone()),
518
519 (Term::Var(v), t) | (t, Term::Var(v)) => {
521 if let Term::Var(v2) = t {
522 if v == v2 {
523 return Some(subst.clone());
524 }
525 }
526 if occurs_in(v, t) {
528 return None;
529 }
530 let mut new_subst = subst.clone();
531 new_subst.insert(v.clone(), t.clone());
532 Some(new_subst)
533 }
534
535 (Term::Fun(f1, args1), Term::Fun(f2, args2)) if f1 == f2 && args1.len() == args2.len() => {
537 let mut current_subst = subst.clone();
538 for (a1, a2) in args1.iter().zip(args2.iter()) {
539 match unify(a1, a2, ¤t_subst) {
540 Some(new_subst) => current_subst = new_subst,
541 None => return None,
542 }
543 }
544 Some(current_subst)
545 }
546
547 (Term::Ref(r1), Term::Ref(r2)) if r1.cid == r2.cid => Some(subst.clone()),
549
550 _ => None,
551 }
552}
553
554pub fn unify_predicates(
556 p1: &Predicate,
557 p2: &Predicate,
558 subst: &Substitution,
559) -> Option<Substitution> {
560 if p1.name != p2.name || p1.args.len() != p2.args.len() {
561 return None;
562 }
563
564 let mut current_subst = subst.clone();
565 for (a1, a2) in p1.args.iter().zip(p2.args.iter()) {
566 match unify(a1, a2, ¤t_subst) {
567 Some(new_subst) => current_subst = new_subst,
568 None => return None,
569 }
570 }
571
572 Some(current_subst)
573}
574
575fn occurs_in(var: &str, term: &Term) -> bool {
577 match term {
578 Term::Var(v) => v == var,
579 Term::Fun(_, args) => args.iter().any(|t| occurs_in(var, t)),
580 _ => false,
581 }
582}
583
584pub fn apply_subst_term(term: &Term, subst: &Substitution) -> Term {
586 match term {
587 Term::Var(v) => subst.get(v).cloned().unwrap_or_else(|| term.clone()),
588 Term::Fun(f, args) => {
589 let new_args = args.iter().map(|t| apply_subst_term(t, subst)).collect();
590 Term::Fun(f.clone(), new_args)
591 }
592 _ => term.clone(),
593 }
594}
595
596pub fn apply_subst_predicate(pred: &Predicate, subst: &Substitution) -> Predicate {
598 Predicate {
599 name: pred.name.clone(),
600 args: pred
601 .args
602 .iter()
603 .map(|t| apply_subst_term(t, subst))
604 .collect(),
605 }
606}
607
608fn rename_rule_vars(rule: &Rule, suffix: usize) -> Rule {
610 let var_map: HashMap<String, String> = rule
611 .variables()
612 .into_iter()
613 .map(|v| (v.clone(), format!("{}_{}", v, suffix)))
614 .collect();
615
616 let rename_subst: Substitution = var_map
617 .into_iter()
618 .map(|(old, new)| (old, Term::Var(new)))
619 .collect();
620
621 Rule {
622 head: apply_subst_predicate(&rule.head, &rename_subst),
623 body: rule
624 .body
625 .iter()
626 .map(|p| apply_subst_predicate(p, &rename_subst))
627 .collect(),
628 }
629}
630
631pub struct MemoizedInferenceEngine {
633 engine: InferenceEngine,
635 cache: Arc<CacheManager>,
637}
638
639impl MemoizedInferenceEngine {
640 pub fn new(cache: Arc<CacheManager>) -> Self {
642 Self {
643 engine: InferenceEngine::new(),
644 cache,
645 }
646 }
647
648 pub fn with_limits(max_depth: usize, max_solutions: usize, cache: Arc<CacheManager>) -> Self {
650 Self {
651 engine: InferenceEngine::with_limits(max_depth, max_solutions),
652 cache,
653 }
654 }
655
656 pub fn query(&self, goal: &Predicate, kb: &KnowledgeBase) -> Result<Vec<Substitution>> {
658 let key = QueryKey::from_predicate(goal);
660
661 if let Some(cached) = self.cache.query_cache.get(&key) {
663 return Ok(cached);
664 }
665
666 let solutions = self.engine.query(goal, kb)?;
668
669 if !solutions.is_empty() {
671 self.cache.query_cache.insert(key, solutions.clone());
672 }
673
674 Ok(solutions)
675 }
676
677 pub fn prove(&self, goal: &Predicate, kb: &KnowledgeBase) -> Result<Option<Proof>> {
679 self.engine.prove(goal, kb)
681 }
682
683 #[inline]
685 pub fn cache_stats(&self) -> crate::cache::CombinedCacheStats {
686 self.cache.stats()
687 }
688
689 pub fn clear_cache(&self) {
691 self.cache.query_cache.clear();
692 }
693}
694
695pub struct DistributedReasoner {
697 engine: InferenceEngine,
699 cache: Option<Arc<CacheManager>>,
701 decompositions: Vec<GoalDecomposition>,
703}
704
705impl DistributedReasoner {
706 pub fn new() -> Result<Self> {
708 Ok(Self {
709 engine: InferenceEngine::new(),
710 cache: None,
711 decompositions: Vec::new(),
712 })
713 }
714
715 pub fn with_cache(cache: Arc<CacheManager>) -> Result<Self> {
717 Ok(Self {
718 engine: InferenceEngine::new(),
719 cache: Some(cache),
720 decompositions: Vec::new(),
721 })
722 }
723
724 pub async fn query(&self, goal: &Predicate, kb: &KnowledgeBase) -> Result<Vec<Substitution>> {
726 if let Some(cache) = &self.cache {
728 let key = QueryKey::from_predicate(goal);
729 if let Some(cached) = cache.query_cache.get(&key) {
730 return Ok(cached);
731 }
732
733 let solutions = self.engine.query(goal, kb)?;
735
736 if !solutions.is_empty() {
738 cache.query_cache.insert(key, solutions.clone());
739 }
740
741 Ok(solutions)
742 } else {
743 self.engine.query(goal, kb)
744 }
745 }
746
747 pub async fn prove(&self, goal: &Predicate, kb: &KnowledgeBase) -> Result<Option<Proof>> {
749 self.engine.prove(goal, kb)
750 }
751
752 pub async fn prove_with_decomposition(
754 &mut self,
755 goal: &Predicate,
756 kb: &KnowledgeBase,
757 ) -> Result<(Option<Proof>, Vec<GoalDecomposition>)> {
758 self.decompositions.clear();
759 let proof = self.prove_tracking(goal, kb, 0)?;
760 let decomps = std::mem::take(&mut self.decompositions);
761 Ok((proof, decomps))
762 }
763
764 fn prove_tracking(
766 &mut self,
767 goal: &Predicate,
768 kb: &KnowledgeBase,
769 depth: usize,
770 ) -> Result<Option<Proof>> {
771 let mut decomp = GoalDecomposition::new(goal.clone(), depth);
773
774 for fact in kb.get_predicates(&goal.name) {
776 if unify_predicates(goal, fact, &Substitution::new()).is_some() {
777 self.decompositions.push(decomp);
778 return Ok(Some(Proof::fact(goal.clone())));
779 }
780 }
781
782 for rule in kb.get_rules(&goal.name) {
784 let renamed_rule = rename_rule_vars(rule, depth);
785
786 if let Some(subst) = unify_predicates(goal, &renamed_rule.head, &Substitution::new()) {
787 decomp.apply_rule(&renamed_rule);
788
789 let mut subproofs = Vec::new();
791 let mut all_proved = true;
792
793 for (i, subgoal) in renamed_rule.body.iter().enumerate() {
794 let subgoal = apply_subst_predicate(subgoal, &subst);
795 if let Some(subproof) = self.prove_tracking(&subgoal, kb, depth + 1)? {
796 subproofs.push(subproof);
797 decomp.mark_solved(i);
798 } else {
799 all_proved = false;
800 break;
801 }
802 }
803
804 if all_proved {
805 self.decompositions.push(decomp);
806 return Ok(Some(Proof::from_rule(
807 goal.clone(),
808 &renamed_rule,
809 subproofs,
810 )));
811 }
812 }
813 }
814
815 self.decompositions.push(decomp);
816 Ok(None)
817 }
818
819 pub fn get_unsolved_goals(&self) -> Vec<&Predicate> {
821 self.decompositions
822 .iter()
823 .flat_map(|d| d.unsolved_subgoals())
824 .collect()
825 }
826
827 pub fn cache_stats(&self) -> Option<crate::cache::CombinedCacheStats> {
829 self.cache.as_ref().map(|c| c.stats())
830 }
831}
832
833impl Default for DistributedReasoner {
834 fn default() -> Self {
835 Self {
836 engine: InferenceEngine::new(),
837 cache: None,
838 decompositions: Vec::new(),
839 }
840 }
841}
842
843#[cfg(test)]
844mod tests {
845 use super::*;
846 use crate::ir::Constant;
847
848 #[test]
849 fn test_unify_constants() {
850 let t1 = Term::Const(Constant::String("Alice".to_string()));
851 let t2 = Term::Const(Constant::String("Alice".to_string()));
852 let subst = Substitution::new();
853
854 assert!(unify(&t1, &t2, &subst).is_some());
855 }
856
857 #[test]
858 fn test_unify_var_const() {
859 let t1 = Term::Var("X".to_string());
860 let t2 = Term::Const(Constant::String("Alice".to_string()));
861 let subst = Substitution::new();
862
863 let result = unify(&t1, &t2, &subst);
864 assert!(result.is_some());
865
866 let result_subst = result.unwrap();
867 assert_eq!(
868 result_subst.get("X"),
869 Some(&Term::Const(Constant::String("Alice".to_string())))
870 );
871 }
872
873 #[test]
874 fn test_unify_functions() {
875 let t1 = Term::Fun(
876 "f".to_string(),
877 vec![Term::Var("X".to_string()), Term::Const(Constant::Int(1))],
878 );
879 let t2 = Term::Fun(
880 "f".to_string(),
881 vec![
882 Term::Const(Constant::String("a".to_string())),
883 Term::Const(Constant::Int(1)),
884 ],
885 );
886 let subst = Substitution::new();
887
888 let result = unify(&t1, &t2, &subst);
889 assert!(result.is_some());
890 }
891
892 #[test]
893 fn test_occurs_check() {
894 let t1 = Term::Var("X".to_string());
895 let t2 = Term::Fun("f".to_string(), vec![Term::Var("X".to_string())]);
896 let subst = Substitution::new();
897
898 assert!(unify(&t1, &t2, &subst).is_none());
899 }
900
901 #[test]
902 fn test_inference_fact() {
903 let mut kb = KnowledgeBase::new();
904 kb.add_fact(Predicate::new(
905 "parent".to_string(),
906 vec![
907 Term::Const(Constant::String("Alice".to_string())),
908 Term::Const(Constant::String("Bob".to_string())),
909 ],
910 ));
911
912 let engine = InferenceEngine::new();
913 let goal = Predicate::new(
914 "parent".to_string(),
915 vec![
916 Term::Const(Constant::String("Alice".to_string())),
917 Term::Var("X".to_string()),
918 ],
919 );
920
921 let solutions = engine.query(&goal, &kb).unwrap();
922 assert_eq!(solutions.len(), 1);
923 assert_eq!(
924 solutions[0].get("X"),
925 Some(&Term::Const(Constant::String("Bob".to_string())))
926 );
927 }
928
929 #[test]
930 fn test_inference_rule() {
931 let mut kb = KnowledgeBase::new();
932
933 kb.add_fact(Predicate::new(
935 "parent".to_string(),
936 vec![
937 Term::Const(Constant::String("alice".to_string())),
938 Term::Const(Constant::String("bob".to_string())),
939 ],
940 ));
941 kb.add_fact(Predicate::new(
942 "parent".to_string(),
943 vec![
944 Term::Const(Constant::String("bob".to_string())),
945 Term::Const(Constant::String("charlie".to_string())),
946 ],
947 ));
948
949 let rule = Rule::new(
951 Predicate::new(
952 "grandparent".to_string(),
953 vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
954 ),
955 vec![
956 Predicate::new(
957 "parent".to_string(),
958 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
959 ),
960 Predicate::new(
961 "parent".to_string(),
962 vec![Term::Var("Y".to_string()), Term::Var("Z".to_string())],
963 ),
964 ],
965 );
966 kb.add_rule(rule);
967
968 let engine = InferenceEngine::new();
969 let goal = Predicate::new(
970 "grandparent".to_string(),
971 vec![
972 Term::Const(Constant::String("alice".to_string())),
973 Term::Var("Z".to_string()),
974 ],
975 );
976
977 let solutions = engine.query(&goal, &kb).unwrap();
978 assert!(!solutions.is_empty());
979 }
980}