Skip to main content

datalog_structural_model/ir/
program.rs

1//! Datalog [`Program`] — an ordered set of [`Rule`]s plus
2//! validation and stratification entry points.
3
4use super::atom::Atom;
5use super::equality_mode::EqualityMode;
6use super::error::ProgramValidationError;
7use super::literal::Literal;
8use super::rule::Rule;
9use super::term::MAX_TRIPLE_TERM_NESTING;
10use crate::stratify::{Stratification, stratify};
11
12/// Datalog program as an ordered set of rules.
13#[derive(Clone, Debug, Default, Eq, PartialEq)]
14pub struct Program {
15    /// Program rules.
16    pub rules: Vec<Rule>,
17}
18
19impl Program {
20    /// Validate all rules in the program for safety and
21    /// stratifiability.
22    ///
23    /// Runs two checks:
24    ///
25    /// 1. **Safety** — every rule must be range-restricted; head variables and
26    ///    variables inside negated body literals must also appear in some
27    ///    positive body literal.
28    /// 2. **Stratifiability** — the predicate dependency graph must have no
29    ///    cycle that includes a negation edge (see [`Program::stratify`]).
30    ///
31    /// # Errors
32    ///
33    /// Returns [`ProgramValidationError::UnsafeRule`] on a safety
34    /// failure, [`ProgramValidationError::UnsafeNegation`] when a
35    /// negated body literal references a variable that no positive
36    /// literal binds, or
37    /// [`ProgramValidationError::UnstratifiableNegation`] when the
38    /// program contains a recursive cycle through negation.
39    pub fn validate(&self) -> Result<(), ProgramValidationError> {
40        self.validate_with_equality_mode(EqualityMode::Default)
41    }
42
43    /// Validate the program under a specified equality-reasoning
44    /// mode (extension).
45    ///
46    /// Runs the same safety / stratification / triple-term-nesting
47    /// checks as [`Self::validate`] and additionally rejects
48    /// negation and aggregation when
49    /// `equality_mode == EqualityMode::SameAs`. The two features
50    /// interact non-monotonically with `owl:sameAs` rewriting; see
51    /// [`EqualityMode`] for the semantic rationale.
52    ///
53    /// # Errors
54    ///
55    /// Returns the same errors as [`Self::validate`] plus
56    /// [`ProgramValidationError::NegationUnderEqualityMode`] or
57    /// [`ProgramValidationError::AggregationUnderEqualityMode`]
58    /// when the corresponding feature is used under
59    /// [`EqualityMode::SameAs`].
60    pub fn validate_with_equality_mode(
61        &self,
62        equality_mode: EqualityMode,
63    ) -> Result<(), ProgramValidationError> {
64        if equality_mode == EqualityMode::SameAs {
65            check_equality_mode_compatibility(&self.rules)?;
66        }
67        for (index, rule) in self.rules.iter().enumerate() {
68            // Triple-term nesting bound — checked before the
69            // safety analyses because deeper nesting indicates a
70            // structural problem unrelated to variable binding.
71            let head_depth = rule.head.nesting_depth();
72            let body_depth = rule
73                .body
74                .iter()
75                .filter_map(Literal::atom)
76                .map(Atom::nesting_depth)
77                .max()
78                .unwrap_or(0);
79            let depth = head_depth.max(body_depth);
80            if depth > MAX_TRIPLE_TERM_NESTING {
81                return Err(ProgramValidationError::TripleTermNestingExceeded {
82                    rule_index: index,
83                    depth,
84                    limit: MAX_TRIPLE_TERM_NESTING,
85                });
86            }
87            let unsafe_head = rule.unsafe_head_variables();
88            if !unsafe_head.is_empty() {
89                return Err(ProgramValidationError::UnsafeRule {
90                    rule_index: index,
91                    variables: unsafe_head,
92                });
93            }
94            let unsafe_neg = rule.unsafe_negative_variables();
95            if !unsafe_neg.is_empty() {
96                return Err(ProgramValidationError::UnsafeNegation {
97                    rule_index: index,
98                    variables: unsafe_neg,
99                });
100            }
101            let unsafe_filter = rule.unsafe_filter_variables();
102            if !unsafe_filter.is_empty() {
103                return Err(ProgramValidationError::UnsafeFilter {
104                    rule_index: index,
105                    variables: unsafe_filter,
106                });
107            }
108            let unsafe_bind = rule.unsafe_bind_variables();
109            if !unsafe_bind.is_empty() {
110                return Err(ProgramValidationError::UnsafeBind {
111                    rule_index: index,
112                    variables: unsafe_bind,
113                });
114            }
115            if rule.has_skolem_in_body() {
116                return Err(ProgramValidationError::SkolemInBodyAtom { rule_index: index });
117            }
118            if rule.has_nested_aggregate_in_inner_body() {
119                return Err(ProgramValidationError::NestedAggregateInInnerBody {
120                    rule_index: index,
121                });
122            }
123            let unsafe_aggregate = rule.unsafe_aggregate_variables();
124            if !unsafe_aggregate.is_empty() {
125                return Err(ProgramValidationError::UnsafeAggregate {
126                    rule_index: index,
127                    variables: unsafe_aggregate,
128                });
129            }
130        }
131        // Stratification check — reject programs with recursion
132        // through negation.
133        self.stratify()?;
134        Ok(())
135    }
136
137    /// Compute the program's stratification.
138    ///
139    /// Builds the predicate dependency graph (edges from each body
140    /// predicate to the head predicate, tagged with polarity),
141    /// collapses strongly connected components, checks that no SCC
142    /// contains an internal negation edge, and assigns each
143    /// predicate to a stratum such that every negation edge goes
144    /// from a lower stratum to a strictly higher one.
145    ///
146    /// # Errors
147    ///
148    /// Returns [`ProgramValidationError::UnstratifiableNegation`]
149    /// when a cycle in the dependency graph contains a negation
150    /// edge.
151    pub fn stratify(&self) -> Result<Stratification, ProgramValidationError> {
152        stratify(self).map_err(ProgramValidationError::from)
153    }
154}
155
156/// extension — reject negation / aggregation under
157/// [`EqualityMode::SameAs`]. Walks each rule's body and surfaces
158/// the first offending literal as
159/// [`ProgramValidationError::NegationUnderEqualityMode`] or
160/// [`ProgramValidationError::AggregationUnderEqualityMode`].
161fn check_equality_mode_compatibility(rules: &[Rule]) -> Result<(), ProgramValidationError> {
162    for (index, rule) in rules.iter().enumerate() {
163        for lit in &rule.body {
164            match lit {
165                Literal::Negative(_) => {
166                    return Err(ProgramValidationError::NegationUnderEqualityMode {
167                        rule_index: index,
168                    });
169                }
170                Literal::Aggregate(_) => {
171                    return Err(ProgramValidationError::AggregationUnderEqualityMode {
172                        rule_index: index,
173                    });
174                }
175                Literal::Positive(_) | Literal::Filter(_) | Literal::Bind { .. } => {}
176            }
177        }
178    }
179    Ok(())
180}