1use crate::ir::{Constant, KnowledgeBase, Predicate, Term};
22use crate::proof_tree::{ProofNode, ProofTree};
23use crate::reasoning::{apply_subst_predicate, rename_rule_vars, unify_predicates, Substitution};
24use futures::future::BoxFuture;
25use ipfrs_core::{Cid, Result};
26use std::collections::HashMap;
27use std::sync::Arc;
28
29pub type Binding = HashMap<String, Term>;
31
32pub struct DistributedBackwardChainer {
34 pub max_depth: usize,
36 pub max_remote_peers: usize,
38 pub timeout_ms: u64,
40}
41
42impl Default for DistributedBackwardChainer {
43 fn default() -> Self {
44 Self {
45 max_depth: 10,
46 max_remote_peers: 3,
47 timeout_ms: 5000,
48 }
49 }
50}
51
52impl DistributedBackwardChainer {
53 pub fn new(max_depth: usize, max_remote_peers: usize, timeout_ms: u64) -> Self {
55 Self {
56 max_depth,
57 max_remote_peers,
58 timeout_ms,
59 }
60 }
61
62 pub async fn prove_with_tree<FP, FQ>(
67 &self,
68 goal: &Term,
69 local_kb: &KnowledgeBase,
70 find_providers: FP,
71 remote_query: FQ,
72 ) -> Result<ProofTree>
73 where
74 FP: Fn(Cid) -> BoxFuture<'static, Vec<String>> + Send + Sync + 'static,
75 FQ: Fn(String, Term) -> BoxFuture<'static, Option<Vec<Binding>>> + Send + Sync + 'static,
76 {
77 let ctx = Arc::new(ProveCtx {
78 max_depth: self.max_depth,
79 max_remote_peers: self.max_remote_peers,
80 find_providers: Arc::new(find_providers),
81 remote_query: Arc::new(remote_query),
82 });
83
84 let root =
85 prove_term_impl(goal.clone(), Substitution::new(), local_kb.clone(), 0, ctx).await;
86
87 let bindings = extract_top_level_bindings(goal, &root);
88 let tree = ProofTree::new(root, goal.clone(), bindings);
89 Ok(tree)
90 }
91}
92
93struct ProveCtx<FP, FQ> {
96 max_depth: usize,
97 max_remote_peers: usize,
98 find_providers: Arc<FP>,
99 remote_query: Arc<FQ>,
100}
101
102fn prove_term_impl<FP, FQ>(
103 goal: Term,
104 subst: Substitution,
105 kb: KnowledgeBase,
106 depth: usize,
107 ctx: Arc<ProveCtx<FP, FQ>>,
108) -> BoxFuture<'static, ProofNode>
109where
110 FP: Fn(Cid) -> BoxFuture<'static, Vec<String>> + Send + Sync + 'static,
111 FQ: Fn(String, Term) -> BoxFuture<'static, Option<Vec<Binding>>> + Send + Sync + 'static,
112{
113 Box::pin(async move {
114 if depth > ctx.max_depth {
115 return ProofNode::unresolved(goal, depth);
116 }
117
118 let pred = match term_to_predicate(&goal) {
119 Some(p) => p,
120 None => {
121 if goal.is_ground() {
122 return ProofNode::fact(goal, depth, None);
123 }
124 return ProofNode::unresolved(goal, depth);
125 }
126 };
127
128 let pred = apply_subst_predicate(&pred, &subst);
129
130 let local_facts: Vec<_> = kb.get_predicates(&pred.name).into_iter().cloned().collect();
132 for fact in &local_facts {
133 if unify_predicates(&pred, fact, &subst).is_some() {
134 return ProofNode::fact(goal, depth, None);
135 }
136 }
137
138 let local_rules: Vec<_> = kb.get_rules(&pred.name).into_iter().cloned().collect();
140 for rule in &local_rules {
141 let renamed = rename_rule_vars(rule, depth);
142 if let Some(new_subst) = unify_predicates(&pred, &renamed.head, &subst) {
143 let mut children = Vec::with_capacity(renamed.body.len());
144 let mut body_resolved = true;
145
146 for body_pred in &renamed.body {
147 let body_term = predicate_to_term(body_pred);
148 let child = prove_term_impl(
149 body_term,
150 new_subst.clone(),
151 kb.clone(),
152 depth + 1,
153 ctx.clone(),
154 )
155 .await;
156 if !child.resolved {
157 body_resolved = false;
158 }
159 children.push(child);
160 }
161
162 if body_resolved {
163 return ProofNode::from_rule(goal, None, children, depth, None);
164 }
165 }
166 }
167
168 if let Some(node) = try_remote_inner(
170 &goal,
171 &subst,
172 &kb,
173 depth,
174 ctx.max_remote_peers,
175 &*ctx.find_providers,
176 &*ctx.remote_query,
177 )
178 .await
179 {
180 return node;
181 }
182
183 ProofNode::unresolved(goal, depth)
184 })
185}
186
187async fn try_remote_inner<FP, FQ>(
188 goal: &Term,
189 subst: &Substitution,
190 kb: &KnowledgeBase,
191 depth: usize,
192 max_remote_peers: usize,
193 find_providers: &FP,
194 remote_query: &FQ,
195) -> Option<ProofNode>
196where
197 FP: Fn(Cid) -> BoxFuture<'static, Vec<String>> + Send + Sync,
198 FQ: Fn(String, Term) -> BoxFuture<'static, Option<Vec<Binding>>> + Send + Sync,
199{
200 let pred = term_to_predicate(goal)?;
201 let pred = apply_subst_predicate(&pred, subst);
202
203 let local_index = kb.index_rules_by_predicate_local();
204 let rule_indices = local_index.get(&pred.name).cloned().unwrap_or_default();
205
206 let mut candidate_cids: Vec<Cid> = Vec::new();
207 for rule_idx in rule_indices {
208 if let Some(rule) = kb.rules.get(rule_idx) {
209 use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
210 if let Ok(rule_ipld) = rule_to_rule_ipld(rule) {
211 if let Ok(cid) = rule_cid(&rule_ipld) {
212 candidate_cids.push(cid);
213 }
214 }
215 }
216 }
217
218 let mut peer_ids: Vec<String> = Vec::new();
219 for cid in candidate_cids {
220 if peer_ids.len() >= max_remote_peers {
221 break;
222 }
223 let providers = find_providers(cid).await;
224 for p in providers {
225 if !peer_ids.contains(&p) {
226 peer_ids.push(p);
227 if peer_ids.len() >= max_remote_peers {
228 break;
229 }
230 }
231 }
232 }
233
234 for peer_id in peer_ids {
235 if let Some(bindings_list) = remote_query(peer_id.clone(), goal.clone()).await {
236 if !bindings_list.is_empty() {
237 return Some(ProofNode::fact(goal.clone(), depth, Some(peer_id)));
238 }
239 }
240 }
241
242 None
243}
244
245fn term_to_predicate(term: &Term) -> Option<Predicate> {
248 match term {
249 Term::Fun(name, args) => Some(Predicate::new(name.clone(), args.clone())),
250 Term::Const(Constant::String(s)) => Some(Predicate::new(s.clone(), Vec::new())),
251 _ => None,
252 }
253}
254
255fn predicate_to_term(pred: &Predicate) -> Term {
256 Term::Fun(pred.name.clone(), pred.args.clone())
257}
258
259fn extract_top_level_bindings(query: &Term, _root: &ProofNode) -> HashMap<String, Term> {
260 let mut bindings = HashMap::new();
261 collect_ground_terms(query, &mut bindings);
262 bindings
263}
264
265fn collect_ground_terms(term: &Term, acc: &mut HashMap<String, Term>) {
266 match term {
267 Term::Fun(_, args) => {
268 for arg in args {
269 collect_ground_terms(arg, acc);
270 }
271 }
272 Term::Const(Constant::String(s)) => {
273 acc.insert(s.clone(), term.clone());
274 }
275 _ => {}
276 }
277}
278
279#[cfg(test)]
284mod tests {
285 use super::*;
286 use crate::ipld_codec::{rule_cid, rule_to_rule_ipld};
287 use crate::ir::{Constant, Predicate, Rule, Term};
288 use std::collections::HashMap;
289
290 fn atom(s: &str) -> Term {
291 Term::Const(Constant::String(s.to_string()))
292 }
293
294 fn var(s: &str) -> Term {
295 Term::Var(s.to_string())
296 }
297
298 fn fun(name: &str, args: Vec<Term>) -> Term {
299 Term::Fun(name.to_string(), args)
300 }
301
302 fn pred(name: &str, args: Vec<Term>) -> Predicate {
303 Predicate::new(name.to_string(), args)
304 }
305
306 fn build_chain_kb() -> KnowledgeBase {
307 let mut kb = KnowledgeBase::new();
308 kb.add_fact(pred("a", vec![atom("alice")]));
309 kb.add_rule(Rule::new(
310 pred("b", vec![var("X")]),
311 vec![pred("a", vec![var("X")])],
312 ));
313 kb.add_rule(Rule::new(
314 pred("c", vec![var("X")]),
315 vec![pred("b", vec![var("X")])],
316 ));
317 kb
318 }
319
320 fn no_providers() -> impl Fn(Cid) -> BoxFuture<'static, Vec<String>> + Send + Sync + 'static {
321 |_cid| Box::pin(async { vec![] })
322 }
323
324 fn no_remote(
325 ) -> impl Fn(String, Term) -> BoxFuture<'static, Option<Vec<Binding>>> + Send + Sync + 'static
326 {
327 |_peer, _goal| Box::pin(async { None })
328 }
329
330 #[tokio::test]
332 async fn test_proof_tree_local_only() {
333 let kb = build_chain_kb();
334 let chainer = DistributedBackwardChainer::default();
335 let goal = fun("c", vec![atom("alice")]);
336 let tree = chainer
337 .prove_with_tree(&goal, &kb, no_providers(), no_remote())
338 .await
339 .expect("prove_with_tree failed");
340
341 assert!(tree.is_complete, "chain should fully resolve locally");
342 assert!(
343 tree.contributing_peers().is_empty(),
344 "no remote peers expected"
345 );
346 assert!(!tree.root.children.is_empty(), "root should have children");
347 assert_eq!(tree.root.peer, None);
348 }
349
350 #[tokio::test]
352 async fn test_proof_tree_partial_remote() {
353 let mut kb = KnowledgeBase::new();
354 kb.add_rule(Rule::new(
355 pred("c", vec![var("X")]),
356 vec![pred("a", vec![var("X")])],
357 ));
358
359 let rule = kb.rules[0].clone();
360 let rule_ipld = rule_to_rule_ipld(&rule).expect("ipld");
361 let expected_cid = rule_cid(&rule_ipld).expect("cid");
362
363 let mock_peer = "mock-peer-001";
364
365 let find_providers = move |lookup_cid: Cid| -> BoxFuture<'static, Vec<String>> {
366 let peers = if lookup_cid == expected_cid {
367 vec![mock_peer.to_string()]
368 } else {
369 vec![]
370 };
371 Box::pin(async move { peers })
372 };
373
374 let remote_query =
375 move |peer: String, _goal: Term| -> BoxFuture<'static, Option<Vec<Binding>>> {
376 let bindings: Option<Vec<Binding>> = if peer == mock_peer {
377 let mut b = HashMap::new();
378 b.insert("X".to_string(), atom("alice"));
379 Some(vec![b])
380 } else {
381 None
382 };
383 Box::pin(async move { bindings })
384 };
385
386 let chainer = DistributedBackwardChainer::default();
387 let goal = fun("c", vec![atom("alice")]);
388 let tree = chainer
389 .prove_with_tree(&goal, &kb, find_providers, remote_query)
390 .await
391 .expect("prove_with_tree failed");
392
393 let peers = tree.contributing_peers();
394 assert!(
395 peers.contains(&mock_peer.to_string()),
396 "mock peer should appear in contributing peers: {:?}",
397 peers
398 );
399 }
400
401 #[tokio::test]
403 async fn test_predicate_index_roundtrip() {
404 let mut kb = KnowledgeBase::new();
405 for i in 0..10 {
406 let head_name = format!("rule_{}", i);
407 kb.add_rule(Rule::new(
408 pred(&head_name, vec![var("X")]),
409 vec![pred("base", vec![var("X")])],
410 ));
411 }
412
413 let mut cid_map: HashMap<usize, Cid> = HashMap::new();
414 for (idx, rule) in kb.rules.iter().enumerate() {
415 let rule_ipld = rule_to_rule_ipld(rule).expect("ipld");
416 let cid = rule_cid(&rule_ipld).expect("cid");
417 cid_map.insert(idx, cid);
418 }
419
420 let index = kb.index_rules_by_predicate(&cid_map);
421
422 for i in 0..10 {
423 let name = format!("rule_{}", i);
424 let cids = index.get(&name).expect("predicate not indexed");
425 assert_eq!(cids.len(), 1, "expected 1 CID for {}", name);
426 }
427
428 assert!(
429 !index.contains_key("base"),
430 "body predicate should not be indexed"
431 );
432 }
433
434 #[tokio::test]
436 async fn test_backward_chain_depth_limit() {
437 let mut kb = KnowledgeBase::new();
438 kb.add_rule(Rule::new(
439 pred("p", vec![var("X")]),
440 vec![pred("p", vec![var("X")])],
441 ));
442
443 let chainer = DistributedBackwardChainer::new(3, 0, 5000);
444 let goal = fun("p", vec![atom("a")]);
445
446 let tree = chainer
447 .prove_with_tree(&goal, &kb, no_providers(), no_remote())
448 .await
449 .expect("prove_with_tree should not error");
450
451 assert!(
452 !tree.is_complete,
453 "recursive chain should NOT be complete when depth limit is hit"
454 );
455 }
456}