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 struct TarjanState<'a> {
127 graph: &'a DependencyGraph,
128 next_index: usize,
129 stack: Vec<String>,
130 indices: HashMap<String, usize>,
131 lowlinks: HashMap<String, usize>,
132 on_stack: HashSet<String>,
133 components: Vec<Vec<String>>,
134 }
135
136 impl TarjanState<'_> {
137 fn strongconnect(&mut self, vertex: &str) {
138 self.indices.insert(vertex.to_string(), self.next_index);
139 self.lowlinks.insert(vertex.to_string(), self.next_index);
140 self.next_index += 1;
141 self.stack.push(vertex.to_string());
142 self.on_stack.insert(vertex.to_string());
143
144 let successors: Vec<String> = self
145 .graph
146 .outgoing(vertex)
147 .iter()
148 .map(|edge| edge.to.clone())
149 .collect();
150 for successor in successors {
151 if !self.indices.contains_key(&successor) {
152 self.strongconnect(&successor);
153 let low_vertex = self.lowlinks[vertex];
154 let low_successor = self.lowlinks[&successor];
155 self.lowlinks
156 .insert(vertex.to_string(), low_vertex.min(low_successor));
157 } else if self.on_stack.contains(&successor) {
158 let low_vertex = self.lowlinks[vertex];
159 let successor_index = self.indices[&successor];
160 self.lowlinks
161 .insert(vertex.to_string(), low_vertex.min(successor_index));
162 }
163 }
164
165 if self.lowlinks[vertex] == self.indices[vertex] {
166 let mut component = Vec::new();
167 loop {
168 let member = self
169 .stack
170 .pop()
171 .expect("the current Tarjan root remains on the traversal stack");
172 self.on_stack.remove(&member);
173 let is_root = member == vertex;
174 component.push(member);
175 if is_root {
176 break;
177 }
178 }
179 self.components.push(component);
180 }
181 }
182 }
183
184 let mut state = TarjanState {
185 graph,
186 next_index: 0,
187 stack: Vec::new(),
188 indices: HashMap::new(),
189 lowlinks: HashMap::new(),
190 on_stack: HashSet::new(),
191 components: Vec::new(),
192 };
193
194 let mut preds_sorted: Vec<&String> = graph.predicates.iter().collect();
198 preds_sorted.sort();
199 for pred in preds_sorted {
200 if !state.indices.contains_key(pred) {
201 state.strongconnect(pred);
202 }
203 }
204
205 state.components
206}
207
208fn check_scc_for_negation_cycle(scc: &[String], graph: &DependencyGraph) -> Option<Vec<String>> {
210 if scc.len() == 1 {
211 let pred = &scc[0];
212 for edge in graph.outgoing(pred) {
213 if edge.to == *pred && edge.dep_type != DepType::Positive {
214 return Some(vec![pred.clone()]);
215 }
216 }
217 return None;
218 }
219
220 let scc_set: HashSet<&str> = scc.iter().map(|s| s.as_str()).collect();
221 for pred in scc {
222 for edge in graph.outgoing(pred) {
223 if scc_set.contains(edge.to.as_str()) && edge.dep_type != DepType::Positive {
224 return Some(scc.to_vec());
225 }
226 }
227 }
228 None
229}
230
231#[derive(Debug, Clone)]
233pub struct Stratum {
234 pub id: usize,
236 pub predicates: Vec<String>,
238}
239
240#[derive(Debug, Clone)]
242pub struct StratificationResult {
243 pub sccs: Vec<Vec<String>>,
245 pub non_monotone_sccs: HashSet<usize>,
247 pub strata: HashMap<String, usize>,
249}
250
251pub fn stratify(program: &Program) -> Result<Vec<Stratum>> {
253 let graph = build_dependency_graph(program);
254 let sccs = find_sccs(&graph);
255
256 for scc in &sccs {
257 if let Some(cycle) = check_scc_for_negation_cycle(scc, &graph) {
258 if !program.is_probabilistic_profile() {
261 return Err(XlogError::StratificationCycle(cycle));
262 }
263 }
267 }
268
269 let mut stratum_map: HashMap<String, usize> = HashMap::new();
270 let mut max_stratum = 0;
271
272 for scc in &sccs {
277 let mut min_stratum = 0;
278 for pred in scc {
279 for edge in graph.outgoing(pred) {
280 if let Some(&dep_stratum) = stratum_map.get(&edge.to) {
281 let required = match edge.dep_type {
282 DepType::Positive => dep_stratum,
283 DepType::Negative | DepType::Aggregate => dep_stratum + 1,
284 };
285 min_stratum = min_stratum.max(required);
286 }
287 }
288 }
289 for pred in scc {
290 stratum_map.insert(pred.clone(), min_stratum);
291 }
292 max_stratum = max_stratum.max(min_stratum);
293 }
294
295 let mut strata: Vec<Stratum> = (0..=max_stratum)
296 .map(|id| Stratum {
297 id,
298 predicates: vec![],
299 })
300 .collect();
301
302 for (pred, stratum) in stratum_map {
303 strata[stratum].predicates.push(pred);
304 }
305
306 for stratum in &mut strata {
309 stratum.predicates.sort();
310 }
311
312 strata.retain(|s| !s.predicates.is_empty());
313 for (i, stratum) in strata.iter_mut().enumerate() {
314 stratum.id = i;
315 }
316
317 Ok(strata)
318}
319
320pub fn analyze_stratification(program: &Program) -> StratificationResult {
323 let graph = build_dependency_graph(program);
324 let sccs = find_sccs(&graph);
325
326 let mut non_monotone_sccs: HashSet<usize> = HashSet::new();
327 for (i, scc) in sccs.iter().enumerate() {
328 if check_scc_for_negation_cycle(scc, &graph).is_some() {
329 non_monotone_sccs.insert(i);
330 }
331 }
332
333 let mut strata: HashMap<String, usize> = HashMap::new();
335 let mut max_stratum = 0;
336
337 for (scc_idx, scc) in sccs.iter().enumerate() {
338 if non_monotone_sccs.contains(&scc_idx) {
339 continue; }
341
342 let mut min_stratum = 0;
343 for pred in scc {
344 for edge in graph.outgoing(pred) {
345 if let Some(&dep_stratum) = strata.get(&edge.to) {
346 let required = match edge.dep_type {
347 DepType::Positive => dep_stratum,
348 DepType::Negative | DepType::Aggregate => dep_stratum + 1,
349 };
350 min_stratum = min_stratum.max(required);
351 }
352 }
353 }
354 for pred in scc {
355 strata.insert(pred.clone(), min_stratum);
356 }
357 max_stratum = max_stratum.max(min_stratum);
358 }
359
360 StratificationResult {
361 sccs,
362 non_monotone_sccs,
363 strata,
364 }
365}
366
367pub fn find_sccs_for_lowering(graph: &DependencyGraph) -> Vec<Vec<String>> {
370 find_sccs(graph)
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::ast::*;
377
378 fn create_tc_program() -> Program {
379 let mut program = Program::new();
380 program.rules.push(Rule {
381 head: Atom {
382 predicate: "edge".into(),
383 terms: vec![Term::Integer(1), Term::Integer(2)],
384 },
385 body: vec![],
386 });
387 program.rules.push(Rule {
388 head: Atom {
389 predicate: "reach".into(),
390 terms: vec![Term::Variable("X".into()), Term::Variable("Y".into())],
391 },
392 body: vec![BodyLiteral::Positive(Atom {
393 predicate: "edge".into(),
394 terms: vec![Term::Variable("X".into()), Term::Variable("Y".into())],
395 })],
396 });
397 program.rules.push(Rule {
398 head: Atom {
399 predicate: "reach".into(),
400 terms: vec![Term::Variable("X".into()), Term::Variable("Z".into())],
401 },
402 body: vec![
403 BodyLiteral::Positive(Atom {
404 predicate: "reach".into(),
405 terms: vec![Term::Variable("X".into()), Term::Variable("Y".into())],
406 }),
407 BodyLiteral::Positive(Atom {
408 predicate: "edge".into(),
409 terms: vec![Term::Variable("Y".into()), Term::Variable("Z".into())],
410 }),
411 ],
412 });
413 program
414 }
415
416 fn create_isolated_program() -> Program {
417 let mut program = Program::new();
418 for i in 1..=3 {
419 program.rules.push(Rule {
420 head: Atom {
421 predicate: "node".into(),
422 terms: vec![Term::Integer(i)],
423 },
424 body: vec![],
425 });
426 }
427 program.rules.push(Rule {
428 head: Atom {
429 predicate: "edge".into(),
430 terms: vec![Term::Integer(1), Term::Integer(2)],
431 },
432 body: vec![],
433 });
434 program.rules.push(Rule {
435 head: Atom {
436 predicate: "isolated".into(),
437 terms: vec![Term::Variable("X".into())],
438 },
439 body: vec![
440 BodyLiteral::Positive(Atom {
441 predicate: "node".into(),
442 terms: vec![Term::Variable("X".into())],
443 }),
444 BodyLiteral::Negated(Atom {
445 predicate: "edge".into(),
446 terms: vec![Term::Variable("X".into()), Term::Variable("Y".into())],
447 }),
448 ],
449 });
450 program
451 }
452
453 fn create_unstratifiable_program() -> Program {
454 let mut program = Program::new();
455 program.rules.push(Rule {
456 head: Atom {
457 predicate: "p".into(),
458 terms: vec![],
459 },
460 body: vec![BodyLiteral::Negated(Atom {
461 predicate: "q".into(),
462 terms: vec![],
463 })],
464 });
465 program.rules.push(Rule {
466 head: Atom {
467 predicate: "q".into(),
468 terms: vec![],
469 },
470 body: vec![BodyLiteral::Negated(Atom {
471 predicate: "p".into(),
472 terms: vec![],
473 })],
474 });
475 program
476 }
477
478 #[test]
479 fn test_stratify_simple() {
480 let program = create_tc_program();
481 let result = stratify(&program);
482 assert!(result.is_ok(), "Stratification failed: {:?}", result.err());
483 }
484
485 #[test]
486 fn test_stratify_with_negation() {
487 let program = create_isolated_program();
488 let result = stratify(&program);
489 assert!(result.is_ok(), "Stratification failed: {:?}", result.err());
490 let strata = result.unwrap();
491 assert!(
492 strata.len() >= 2,
493 "Expected at least 2 strata, got {}",
494 strata.len()
495 );
496 }
497
498 #[test]
499 fn test_stratify_cycle_through_negation() {
500 let program = create_unstratifiable_program();
501 let result = stratify(&program);
502 assert!(result.is_err(), "Should fail with cycle through negation");
503 if let Err(XlogError::StratificationCycle(preds)) = result {
504 assert!(preds.contains(&"p".to_string()) || preds.contains(&"q".to_string()));
505 }
506 }
507
508 #[test]
509 fn test_stratify_probabilistic_non_monotone_allows_exact_ddnnf() {
510 let mut program = create_unstratifiable_program();
512 program.directives.prob_engine = Some(ProbEngine::ExactDdnnf);
513
514 let result = stratify(&program);
515 assert!(
516 result.is_ok(),
517 "Expected exact_ddnnf to allow non-monotone recursion (via WFS), got: {:?}",
518 result.err()
519 );
520 }
521
522 #[test]
523 fn test_stratify_probabilistic_non_monotone_allows_mc() {
524 let mut program = create_unstratifiable_program();
525 program.directives.prob_engine = Some(ProbEngine::Mc);
526
527 let result = stratify(&program);
528 assert!(
529 result.is_ok(),
530 "Expected mc to allow non-monotone recursion, got: {:?}",
531 result.err()
532 );
533 }
534
535 #[test]
536 fn test_dependency_graph_construction() {
537 let program = create_tc_program();
538 let graph = build_dependency_graph(&program);
539 assert!(graph.predicates.contains("edge"));
540 assert!(graph.predicates.contains("reach"));
541 let reach_deps = graph.outgoing("reach");
542 assert!(!reach_deps.is_empty());
543 }
544
545 #[test]
546 fn test_analyze_stratification_detects_non_monotone() {
547 let program = create_unstratifiable_program(); let result = analyze_stratification(&program);
549
550 assert!(
551 !result.non_monotone_sccs.is_empty(),
552 "Should detect non-monotone SCC"
553 );
554 let has_non_monotone = result.sccs.iter().enumerate().any(|(i, scc)| {
556 result.non_monotone_sccs.contains(&i)
557 && (scc.contains(&"p".to_string()) || scc.contains(&"q".to_string()))
558 });
559 assert!(has_non_monotone, "SCC with p/q should be non-monotone");
560 }
561
562 #[test]
563 fn test_analyze_stratification_stratified_program() {
564 let program = create_isolated_program(); let result = analyze_stratification(&program);
566
567 assert!(
568 result.non_monotone_sccs.is_empty(),
569 "Stratified program has no non-monotone SCCs"
570 );
571 assert!(
572 result.strata.contains_key("isolated"),
573 "isolated should have a stratum"
574 );
575 assert!(
576 result.strata.contains_key("edge"),
577 "edge should have a stratum"
578 );
579
580 let isolated_stratum = result.strata.get("isolated").unwrap();
582 let edge_stratum = result.strata.get("edge").unwrap();
583 assert!(
584 isolated_stratum > edge_stratum,
585 "isolated should be in higher stratum than edge"
586 );
587 }
588}