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