1use crate::ast::{BodyLiteral, Program};
4use std::collections::{HashMap, HashSet};
5use xlog_core::{Result, XlogError};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub(crate) enum DepType {
10 Positive,
11 Negative,
12 Aggregate,
13}
14
15#[derive(Debug, Clone)]
17pub(crate) struct DepEdge {
18 pub from: String,
19 pub to: String,
20 pub dep_type: DepType,
21}
22
23#[derive(Debug, Default)]
25pub struct DependencyGraph {
26 pub predicates: HashSet<String>,
28 edges: Vec<DepEdge>,
32 adjacency: std::collections::HashMap<String, Vec<usize>>,
35}
36
37impl DependencyGraph {
38 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn add_predicate(&mut self, name: String) {
45 self.predicates.insert(name);
46 }
47
48 pub(crate) fn add_edge(&mut self, from: String, to: String, dep_type: DepType) {
49 self.predicates.insert(from.clone());
50 self.predicates.insert(to.clone());
51 self.adjacency
52 .entry(from.clone())
53 .or_default()
54 .push(self.edges.len());
55 self.edges.push(DepEdge { from, to, dep_type });
56 }
57
58 pub(crate) fn outgoing(&self, pred: &str) -> Vec<&DepEdge> {
59 match self.adjacency.get(pred) {
60 Some(idxs) => idxs
61 .iter()
62 .map(|&i| {
63 let edge = &self.edges[i];
64 debug_assert_eq!(edge.from, pred, "adjacency index out of sync");
65 edge
66 })
67 .collect(),
68 None => Vec::new(),
69 }
70 }
71}
72
73pub fn build_dependency_graph(program: &Program) -> DependencyGraph {
75 let mut graph = DependencyGraph::new();
76
77 for rule in &program.rules {
78 let head = &rule.head.predicate;
79 graph.add_predicate(head.clone());
80
81 for lit in &rule.body {
82 match lit {
83 BodyLiteral::Positive(atom) => {
84 graph.add_edge(head.clone(), atom.predicate.clone(), DepType::Positive);
85 }
86 BodyLiteral::Negated(atom) => {
87 graph.add_edge(head.clone(), atom.predicate.clone(), DepType::Negative);
88 }
89 BodyLiteral::Epistemic(lit) => {
90 graph.add_edge(head.clone(), lit.atom.predicate.clone(), DepType::Negative);
91 }
92 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
93 }
94 }
95
96 if rule.has_aggregation() {
97 for lit in &rule.body {
98 if let BodyLiteral::Positive(atom) = lit {
99 graph.add_edge(head.clone(), atom.predicate.clone(), DepType::Aggregate);
100 }
101 }
102 }
103 }
104
105 for lr in &program.learnable_rules {
110 let head = &lr.head.predicate;
111 graph.add_predicate(head.clone());
112 for body_lit in &lr.body {
113 if let Some(atom) = body_lit.atom() {
114 graph.add_predicate(atom.predicate.clone());
115 graph.add_edge(head.clone(), atom.predicate.clone(), DepType::Positive);
116 }
117 }
118 }
119
120 graph
121}
122
123fn find_sccs(graph: &DependencyGraph) -> Vec<Vec<String>> {
126 let mut index_counter = 0;
127 let mut stack = Vec::new();
128 let mut indices: HashMap<String, usize> = HashMap::new();
129 let mut lowlinks: HashMap<String, usize> = HashMap::new();
130 let mut on_stack: HashSet<String> = HashSet::new();
131 let mut sccs: Vec<Vec<String>> = Vec::new();
132
133 #[allow(clippy::too_many_arguments)]
134 fn strongconnect(
135 v: &str,
136 graph: &DependencyGraph,
137 index_counter: &mut usize,
138 stack: &mut Vec<String>,
139 indices: &mut HashMap<String, usize>,
140 lowlinks: &mut HashMap<String, usize>,
141 on_stack: &mut HashSet<String>,
142 sccs: &mut Vec<Vec<String>>,
143 ) {
144 indices.insert(v.to_string(), *index_counter);
145 lowlinks.insert(v.to_string(), *index_counter);
146 *index_counter += 1;
147 stack.push(v.to_string());
148 on_stack.insert(v.to_string());
149
150 for edge in graph.outgoing(v) {
151 let w = &edge.to;
152 if !indices.contains_key(w) {
153 strongconnect(
154 w,
155 graph,
156 index_counter,
157 stack,
158 indices,
159 lowlinks,
160 on_stack,
161 sccs,
162 );
163 let low_v = *lowlinks.get(v).unwrap();
164 let low_w = *lowlinks.get(w).unwrap();
165 lowlinks.insert(v.to_string(), low_v.min(low_w));
166 } else if on_stack.contains(w) {
167 let low_v = *lowlinks.get(v).unwrap();
168 let idx_w = *indices.get(w).unwrap();
169 lowlinks.insert(v.to_string(), low_v.min(idx_w));
170 }
171 }
172
173 let low_v = *lowlinks.get(v).unwrap();
174 let idx_v = *indices.get(v).unwrap();
175 if low_v == idx_v {
176 let mut scc = Vec::new();
177 loop {
178 let w = stack.pop().unwrap();
179 on_stack.remove(&w);
180 scc.push(w.clone());
181 if w == v {
182 break;
183 }
184 }
185 sccs.push(scc);
186 }
187 }
188
189 let mut preds_sorted: Vec<&String> = graph.predicates.iter().collect();
193 preds_sorted.sort();
194 for pred in preds_sorted {
195 if !indices.contains_key(pred) {
196 strongconnect(
197 pred,
198 graph,
199 &mut index_counter,
200 &mut stack,
201 &mut indices,
202 &mut lowlinks,
203 &mut on_stack,
204 &mut sccs,
205 );
206 }
207 }
208
209 sccs
210}
211
212fn check_scc_for_negation_cycle(scc: &[String], graph: &DependencyGraph) -> Option<Vec<String>> {
214 if scc.len() == 1 {
215 let pred = &scc[0];
216 for edge in graph.outgoing(pred) {
217 if edge.to == *pred && edge.dep_type != DepType::Positive {
218 return Some(vec![pred.clone()]);
219 }
220 }
221 return None;
222 }
223
224 let scc_set: HashSet<&str> = scc.iter().map(|s| s.as_str()).collect();
225 for pred in scc {
226 for edge in graph.outgoing(pred) {
227 if scc_set.contains(edge.to.as_str()) && edge.dep_type != DepType::Positive {
228 return Some(scc.to_vec());
229 }
230 }
231 }
232 None
233}
234
235#[derive(Debug, Clone)]
237pub struct Stratum {
238 pub id: usize,
240 pub predicates: Vec<String>,
242}
243
244#[derive(Debug, Clone)]
246pub struct StratificationResult {
247 pub sccs: Vec<Vec<String>>,
249 pub non_monotone_sccs: HashSet<usize>,
251 pub strata: HashMap<String, usize>,
253}
254
255pub fn stratify(program: &Program) -> Result<Vec<Stratum>> {
257 let graph = build_dependency_graph(program);
258 let sccs = find_sccs(&graph);
259
260 for scc in &sccs {
261 if let Some(cycle) = check_scc_for_negation_cycle(scc, &graph) {
262 if !program.is_probabilistic_profile() {
265 return Err(XlogError::StratificationCycle(cycle));
266 }
267 }
271 }
272
273 let mut stratum_map: HashMap<String, usize> = HashMap::new();
274 let mut max_stratum = 0;
275
276 for scc in &sccs {
281 let mut min_stratum = 0;
282 for pred in scc {
283 for edge in graph.outgoing(pred) {
284 if let Some(&dep_stratum) = stratum_map.get(&edge.to) {
285 let required = match edge.dep_type {
286 DepType::Positive => dep_stratum,
287 DepType::Negative | DepType::Aggregate => dep_stratum + 1,
288 };
289 min_stratum = min_stratum.max(required);
290 }
291 }
292 }
293 for pred in scc {
294 stratum_map.insert(pred.clone(), min_stratum);
295 }
296 max_stratum = max_stratum.max(min_stratum);
297 }
298
299 let mut strata: Vec<Stratum> = (0..=max_stratum)
300 .map(|id| Stratum {
301 id,
302 predicates: vec![],
303 })
304 .collect();
305
306 for (pred, stratum) in stratum_map {
307 strata[stratum].predicates.push(pred);
308 }
309
310 for stratum in &mut strata {
313 stratum.predicates.sort();
314 }
315
316 strata.retain(|s| !s.predicates.is_empty());
317 for (i, stratum) in strata.iter_mut().enumerate() {
318 stratum.id = i;
319 }
320
321 Ok(strata)
322}
323
324pub fn analyze_stratification(program: &Program) -> StratificationResult {
327 let graph = build_dependency_graph(program);
328 let sccs = find_sccs(&graph);
329
330 let mut non_monotone_sccs: HashSet<usize> = HashSet::new();
331 for (i, scc) in sccs.iter().enumerate() {
332 if check_scc_for_negation_cycle(scc, &graph).is_some() {
333 non_monotone_sccs.insert(i);
334 }
335 }
336
337 let mut strata: HashMap<String, usize> = HashMap::new();
339 let mut max_stratum = 0;
340
341 for (scc_idx, scc) in sccs.iter().enumerate() {
342 if non_monotone_sccs.contains(&scc_idx) {
343 continue; }
345
346 let mut min_stratum = 0;
347 for pred in scc {
348 for edge in graph.outgoing(pred) {
349 if let Some(&dep_stratum) = strata.get(&edge.to) {
350 let required = match edge.dep_type {
351 DepType::Positive => dep_stratum,
352 DepType::Negative | DepType::Aggregate => dep_stratum + 1,
353 };
354 min_stratum = min_stratum.max(required);
355 }
356 }
357 }
358 for pred in scc {
359 strata.insert(pred.clone(), min_stratum);
360 }
361 max_stratum = max_stratum.max(min_stratum);
362 }
363
364 StratificationResult {
365 sccs,
366 non_monotone_sccs,
367 strata,
368 }
369}
370
371pub fn find_sccs_for_lowering(graph: &DependencyGraph) -> Vec<Vec<String>> {
374 find_sccs(graph)
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::ast::*;
381
382 fn create_tc_program() -> Program {
383 let mut program = Program::new();
384 program.rules.push(Rule {
385 head: Atom {
386 predicate: "edge".into(),
387 terms: vec![Term::Integer(1), Term::Integer(2)],
388 },
389 body: vec![],
390 });
391 program.rules.push(Rule {
392 head: Atom {
393 predicate: "reach".into(),
394 terms: vec![Term::Variable("X".into()), Term::Variable("Y".into())],
395 },
396 body: vec![BodyLiteral::Positive(Atom {
397 predicate: "edge".into(),
398 terms: vec![Term::Variable("X".into()), Term::Variable("Y".into())],
399 })],
400 });
401 program.rules.push(Rule {
402 head: Atom {
403 predicate: "reach".into(),
404 terms: vec![Term::Variable("X".into()), Term::Variable("Z".into())],
405 },
406 body: vec![
407 BodyLiteral::Positive(Atom {
408 predicate: "reach".into(),
409 terms: vec![Term::Variable("X".into()), Term::Variable("Y".into())],
410 }),
411 BodyLiteral::Positive(Atom {
412 predicate: "edge".into(),
413 terms: vec![Term::Variable("Y".into()), Term::Variable("Z".into())],
414 }),
415 ],
416 });
417 program
418 }
419
420 fn create_isolated_program() -> Program {
421 let mut program = Program::new();
422 for i in 1..=3 {
423 program.rules.push(Rule {
424 head: Atom {
425 predicate: "node".into(),
426 terms: vec![Term::Integer(i)],
427 },
428 body: vec![],
429 });
430 }
431 program.rules.push(Rule {
432 head: Atom {
433 predicate: "edge".into(),
434 terms: vec![Term::Integer(1), Term::Integer(2)],
435 },
436 body: vec![],
437 });
438 program.rules.push(Rule {
439 head: Atom {
440 predicate: "isolated".into(),
441 terms: vec![Term::Variable("X".into())],
442 },
443 body: vec![
444 BodyLiteral::Positive(Atom {
445 predicate: "node".into(),
446 terms: vec![Term::Variable("X".into())],
447 }),
448 BodyLiteral::Negated(Atom {
449 predicate: "edge".into(),
450 terms: vec![Term::Variable("X".into()), Term::Variable("Y".into())],
451 }),
452 ],
453 });
454 program
455 }
456
457 fn create_unstratifiable_program() -> Program {
458 let mut program = Program::new();
459 program.rules.push(Rule {
460 head: Atom {
461 predicate: "p".into(),
462 terms: vec![],
463 },
464 body: vec![BodyLiteral::Negated(Atom {
465 predicate: "q".into(),
466 terms: vec![],
467 })],
468 });
469 program.rules.push(Rule {
470 head: Atom {
471 predicate: "q".into(),
472 terms: vec![],
473 },
474 body: vec![BodyLiteral::Negated(Atom {
475 predicate: "p".into(),
476 terms: vec![],
477 })],
478 });
479 program
480 }
481
482 #[test]
483 fn test_stratify_simple() {
484 let program = create_tc_program();
485 let result = stratify(&program);
486 assert!(result.is_ok(), "Stratification failed: {:?}", result.err());
487 }
488
489 #[test]
490 fn test_stratify_with_negation() {
491 let program = create_isolated_program();
492 let result = stratify(&program);
493 assert!(result.is_ok(), "Stratification failed: {:?}", result.err());
494 let strata = result.unwrap();
495 assert!(
496 strata.len() >= 2,
497 "Expected at least 2 strata, got {}",
498 strata.len()
499 );
500 }
501
502 #[test]
503 fn test_stratify_cycle_through_negation() {
504 let program = create_unstratifiable_program();
505 let result = stratify(&program);
506 assert!(result.is_err(), "Should fail with cycle through negation");
507 if let Err(XlogError::StratificationCycle(preds)) = result {
508 assert!(preds.contains(&"p".to_string()) || preds.contains(&"q".to_string()));
509 }
510 }
511
512 #[test]
513 fn test_stratify_probabilistic_non_monotone_allows_exact_ddnnf() {
514 let mut program = create_unstratifiable_program();
516 program.directives.prob_engine = Some(ProbEngine::ExactDdnnf);
517
518 let result = stratify(&program);
519 assert!(
520 result.is_ok(),
521 "Expected exact_ddnnf to allow non-monotone recursion (via WFS), got: {:?}",
522 result.err()
523 );
524 }
525
526 #[test]
527 fn test_stratify_probabilistic_non_monotone_allows_mc() {
528 let mut program = create_unstratifiable_program();
529 program.directives.prob_engine = Some(ProbEngine::Mc);
530
531 let result = stratify(&program);
532 assert!(
533 result.is_ok(),
534 "Expected mc to allow non-monotone recursion, got: {:?}",
535 result.err()
536 );
537 }
538
539 #[test]
540 fn test_dependency_graph_construction() {
541 let program = create_tc_program();
542 let graph = build_dependency_graph(&program);
543 assert!(graph.predicates.contains("edge"));
544 assert!(graph.predicates.contains("reach"));
545 let reach_deps = graph.outgoing("reach");
546 assert!(!reach_deps.is_empty());
547 }
548
549 #[test]
550 fn test_analyze_stratification_detects_non_monotone() {
551 let program = create_unstratifiable_program(); let result = analyze_stratification(&program);
553
554 assert!(
555 !result.non_monotone_sccs.is_empty(),
556 "Should detect non-monotone SCC"
557 );
558 let has_non_monotone = result.sccs.iter().enumerate().any(|(i, scc)| {
560 result.non_monotone_sccs.contains(&i)
561 && (scc.contains(&"p".to_string()) || scc.contains(&"q".to_string()))
562 });
563 assert!(has_non_monotone, "SCC with p/q should be non-monotone");
564 }
565
566 #[test]
567 fn test_analyze_stratification_stratified_program() {
568 let program = create_isolated_program(); let result = analyze_stratification(&program);
570
571 assert!(
572 result.non_monotone_sccs.is_empty(),
573 "Stratified program has no non-monotone SCCs"
574 );
575 assert!(
576 result.strata.contains_key("isolated"),
577 "isolated should have a stratum"
578 );
579 assert!(
580 result.strata.contains_key("edge"),
581 "edge should have a stratum"
582 );
583
584 let isolated_stratum = result.strata.get("isolated").unwrap();
586 let edge_stratum = result.strata.get("edge").unwrap();
587 assert!(
588 isolated_stratum > edge_stratum,
589 "isolated should be in higher stratum than edge"
590 );
591 }
592}