1use super::*;
2
3pub(crate) type EvalFn = fn(&str, &[LogicalTerm]) -> Result<bool, String>;
7pub(crate) type BatchEvalFn = fn(&[ComputeRequest]) -> Vec<Result<bool, String>>;
9
10pub(super) fn extract_num_value(
11 term: &LogicalTerm,
12 subs: &HashMap<String, GroundTerm>,
13) -> Option<f64> {
14 match term {
15 LogicalTerm::Number(n) => Some(*n),
16 LogicalTerm::Variable(v) => {
17 let gt = subs.get(v.as_str())?;
18 gt.as_f64()
19 }
20 _ => None,
21 }
22}
23
24pub(super) fn try_numeric_comparison(
32 rel: &str,
33 args: &[LogicalTerm],
34 subs: &HashMap<String, GroundTerm>,
35) -> Option<QueryResult> {
36 let a = extract_num_value(args.get(0)?, subs)?;
37 let b = extract_num_value(args.get(1)?, subs)?;
38 let holds = match rel {
39 "greater" => a > b,
40 "less" => a < b,
41 "num_equal" => a == b,
42 _ => return None,
43 };
44 if !a.is_finite() || !b.is_finite() {
45 return Some(QueryResult::Unknown(UnknownReason::NonFinite));
46 }
47 Some(if holds {
48 QueryResult::True
49 } else {
50 QueryResult::False
51 })
52}
53
54pub(super) fn try_arithmetic_evaluation(
55 rel: &str,
56 args: &[LogicalTerm],
57 subs: &HashMap<String, GroundTerm>,
58) -> Option<bool> {
59 let x1 = extract_num_value(args.get(0)?, subs)?;
60 let x2 = extract_num_value(args.get(1)?, subs)?;
61 let x3 = extract_num_value(args.get(2)?, subs)?;
62 nibli_types::eval_arithmetic(rel, &[x1, x2, x3])
65}
66
67pub(super) fn ground_term_to_logical_term(gt: &GroundTerm) -> LogicalTerm {
69 match gt {
70 GroundTerm::Constant(c) => LogicalTerm::Constant(c.clone()),
71 GroundTerm::Number(bits) => LogicalTerm::Number(f64::from_bits(*bits)),
72 GroundTerm::Description(d) => LogicalTerm::Description(d.clone()),
73 GroundTerm::Unspecified => LogicalTerm::Unspecified,
74 GroundTerm::PatternVar(v) => LogicalTerm::Variable(v.clone()),
75 GroundTerm::SkolemFn(name, _) => LogicalTerm::Constant(name.clone()),
76 GroundTerm::DepPair(_, _) => LogicalTerm::Unspecified,
77 }
78}
79
80pub(super) fn witness_term_to_logical_term(gt: &GroundTerm) -> LogicalTerm {
86 match gt {
87 GroundTerm::SkolemFn(..) | GroundTerm::DepPair(..) => {
88 LogicalTerm::Constant(gt.to_display_string())
89 }
90 other => ground_term_to_logical_term(other),
91 }
92}
93
94pub(super) struct NumericGroupVerdict {
107 pub relation: String,
108 pub method: &'static str,
111 pub verdict: QueryResult,
112}
113
114pub(super) fn try_evaluate_numeric_group(
141 inner: &KnowledgeBaseInner,
142 buffer: &LogicBuffer,
143 exists_var: &str,
144 body_id: u32,
145 subs: &HashMap<String, GroundTerm>,
146) -> Option<NumericGroupVerdict> {
147 let mut conjuncts: Vec<u32> = Vec::new();
149 let mut stack = vec![body_id];
150 while let Some(id) = stack.pop() {
151 match get_node(buffer, id).ok()? {
152 LogicNode::AndNode((l, r)) => {
153 stack.push(*l);
154 stack.push(*r);
155 }
156 LogicNode::Predicate(_) | LogicNode::ComputeNode(_) => conjuncts.push(id),
157 _ => return None,
158 }
159 }
160
161 let is_head_var =
163 |args: &[LogicalTerm]| matches!(args, [LogicalTerm::Variable(v)] if v == exists_var);
164 let mut head: Option<(&str, bool)> = None; for &id in &conjuncts {
166 match get_node(buffer, id).ok()? {
167 LogicNode::ComputeNode((rel, args)) if is_head_var(args) => {
168 if head.is_some() {
169 return None; }
171 head = Some((rel.as_str(), true));
172 }
173 LogicNode::Predicate((rel, args))
174 if is_head_var(args)
175 && nibli_types::relations::is_numeric_comparison(rel.as_str()) =>
176 {
177 if head.is_some() {
178 return None;
179 }
180 head = Some((rel.as_str(), false));
181 }
182 _ => {}
183 }
184 }
185 let (rel, head_is_compute) = head?;
186
187 let role_prefix = format!("{rel}_x");
189 let mut roles: Vec<Option<&LogicalTerm>> = Vec::new();
190 for &id in &conjuncts {
191 match get_node(buffer, id).ok()? {
192 LogicNode::ComputeNode((r, args)) if is_head_var(args) && r.as_str() == rel => {}
193 LogicNode::Predicate((r, args)) if is_head_var(args) && r.as_str() == rel => {}
194 LogicNode::Predicate((r, args)) if r.starts_with(&role_prefix) => {
195 let n: usize = r[role_prefix.len()..].parse().ok()?;
196 if n == 0 {
197 return None;
198 }
199 match args.as_slice() {
200 [LogicalTerm::Variable(v), arg] if v == exists_var => {
201 if roles.len() < n {
202 roles.resize(n, None);
203 }
204 if roles[n - 1].is_some() {
205 return None; }
207 roles[n - 1] = Some(arg);
208 }
209 _ => return None,
210 }
211 }
212 _ => return None, }
214 }
215 if roles.is_empty() || roles.iter().any(|r| r.is_none()) {
217 return None;
218 }
219 let collected: Vec<LogicalTerm> = roles.into_iter().map(|r| r.unwrap().clone()).collect();
220
221 {
228 let operands: Vec<f64> = collected
229 .iter()
230 .filter_map(|t| extract_num_value(t, subs))
231 .collect();
232 let non_finite = match rel {
233 r if nibli_types::relations::is_builtin_arithmetic(r) => {
234 operands.len() == 3 && nibli_types::eval_arithmetic(rel, &operands).is_none()
235 }
236 r if nibli_types::relations::is_numeric_comparison(r) => {
237 operands.len() >= 2 && operands.iter().take(2).any(|n| !n.is_finite())
238 }
239 _ => false,
240 };
241 if non_finite {
242 return Some(NumericGroupVerdict {
243 relation: rel.to_string(),
244 method: "non_finite",
245 verdict: QueryResult::Unknown(UnknownReason::NonFinite),
246 });
247 }
248 }
249
250 if let Some(verdict) = try_numeric_comparison(rel, &collected, subs) {
254 return Some(NumericGroupVerdict {
255 relation: rel.to_string(),
256 method: if matches!(verdict, QueryResult::Unknown(_)) {
257 "non_finite"
258 } else {
259 "numeric"
260 },
261 verdict,
262 });
263 }
264 if let Some(holds) = try_arithmetic_evaluation(rel, &collected, subs) {
265 return Some(NumericGroupVerdict {
266 relation: rel.to_string(),
267 method: "arithmetic",
268 verdict: bool_verdict(holds),
269 });
270 }
271 if head_is_compute {
272 let resolved = resolve_args_for_dispatch(&collected, subs);
274 let dispatchable = resolved
275 .iter()
276 .all(|t| matches!(t, LogicalTerm::Number(_) | LogicalTerm::Unspecified));
277 if dispatchable {
278 return Some(match dispatch_to_backend(inner, rel, &resolved) {
279 Ok(holds) => NumericGroupVerdict {
280 relation: rel.to_string(),
281 method: "backend",
282 verdict: bool_verdict(holds),
283 },
284 Err(_) => NumericGroupVerdict {
289 relation: rel.to_string(),
290 method: "backend_unavailable",
291 verdict: QueryResult::Unknown(UnknownReason::BackendUnavailable),
292 },
293 });
294 }
295 }
296 None
297}
298
299fn bool_verdict(holds: bool) -> QueryResult {
301 if holds {
302 QueryResult::True
303 } else {
304 QueryResult::False
305 }
306}
307
308pub(super) fn resolve_args_for_dispatch(
309 args: &[LogicalTerm],
310 subs: &HashMap<String, GroundTerm>,
311) -> Vec<LogicalTerm> {
312 args.iter()
313 .map(|a| match a {
314 LogicalTerm::Variable(v) => {
315 if let Some(gt) = subs.get(v.as_str()) {
316 ground_term_to_logical_term(gt)
317 } else {
318 a.clone()
319 }
320 }
321 _ => a.clone(),
322 })
323 .collect()
324}
325
326pub(super) fn dispatch_to_backend(
335 inner: &KnowledgeBaseInner,
336 rel: &str,
337 args: &[LogicalTerm],
338) -> Result<bool, String> {
339 match inner.compute_eval {
340 Some(eval) => eval(rel, args),
341 None => Err("Compute backend not registered".to_string()),
342 }
343}
344
345pub struct ComputeRequest {
347 pub relation: String,
348 pub args: Vec<LogicalTerm>,
349}
350
351fn dispatch_batch_to_backend(
352 inner: &KnowledgeBaseInner,
353 requests: &[ComputeRequest],
354) -> Vec<Result<bool, String>> {
355 match inner.compute_batch_eval {
356 Some(batch_eval) => batch_eval(requests),
357 None => requests
358 .iter()
359 .map(|_| Err("Compute backend not registered".to_string()))
360 .collect(),
361 }
362}
363
364pub(super) fn build_ground_fact_from_resolved(
366 rel: &str,
367 resolved_args: &[LogicalTerm],
368) -> Option<StoredFact> {
369 for arg in resolved_args {
370 if matches!(arg, LogicalTerm::Variable(_)) {
371 return None;
372 }
373 }
374 let args: Vec<GroundTerm> = resolved_args
375 .iter()
376 .map(|arg| match arg {
377 LogicalTerm::Number(n) => GroundTerm::from_f64(*n),
378 LogicalTerm::Constant(c) => GroundTerm::Constant(c.clone()),
379 LogicalTerm::Description(d) => GroundTerm::Description(d.clone()),
380 LogicalTerm::Unspecified => GroundTerm::Unspecified,
381 LogicalTerm::Variable(v) => {
382 unreachable!("Variable '{}' in compute result — should be ground", v)
383 }
384 })
385 .collect();
386 Some(StoredFact::Bare(GroundFact::new(rel, args)))
387}
388
389pub(super) struct BatchComputeResult {
393 pub results: Vec<bool>,
394 pub deferred_facts: Vec<StoredFact>,
395}
396
397pub(super) fn batch_evaluate_compute_for_members(
398 inner: &KnowledgeBaseInner,
399 rel: &str,
400 args: &[LogicalTerm],
401 var: &str,
402 members: &[GroundTerm],
403 subs: &HashMap<String, GroundTerm>,
404) -> Option<BatchComputeResult> {
405 let mut results = vec![false; members.len()];
406 let mut deferred_facts = Vec::new();
407 let mut pending: Vec<(usize, Vec<LogicalTerm>)> = Vec::new();
408
409 for (i, member) in members.iter().enumerate() {
410 let mut s = subs.clone();
411 s.insert(var.to_string(), member.clone());
412
413 if let Some(r) = try_arithmetic_evaluation(rel, args, &s) {
414 results[i] = r;
415 if r {
416 let resolved = resolve_args_for_dispatch(args, &s);
417 if let Some(fact) = build_ground_fact_from_resolved(rel, &resolved) {
418 deferred_facts.push(fact);
419 }
420 }
421 } else {
422 let resolved = resolve_args_for_dispatch(args, &s);
423 pending.push((i, resolved));
424 }
425 }
426
427 if pending.is_empty() {
428 return Some(BatchComputeResult {
429 results,
430 deferred_facts,
431 });
432 }
433
434 let requests: Vec<ComputeRequest> = pending
435 .iter()
436 .map(|(_, resolved)| ComputeRequest {
437 relation: rel.to_string(),
438 args: resolved.clone(),
439 })
440 .collect();
441 let batch_results = dispatch_batch_to_backend(inner, &requests);
442
443 for (batch_idx, result) in batch_results.into_iter().enumerate() {
444 let member_idx = pending[batch_idx].0;
445 match result {
446 Ok(r) => {
447 results[member_idx] = r;
448 if r {
449 if let Some(fact) = build_ground_fact_from_resolved(rel, &pending[batch_idx].1)
450 {
451 deferred_facts.push(fact);
452 }
453 }
454 }
455 Err(_) => return None,
456 }
457 }
458 Some(BatchComputeResult {
459 results,
460 deferred_facts,
461 })
462}