Skip to main content

datalog_structural_model/ir/
literal.rs

1//! Body literal — positive/negated atom, filter, `BIND`, or
2//! aggregation.
3
4use super::aggregate::AggregateBody;
5use super::atom::Atom;
6use super::bind::BindExpr;
7use super::filter::BuiltinFilter;
8use super::symbols::VariableSymbol;
9
10/// Datalog body literal — a positive or negated atom, a built-in
11/// filter expression, or a built-in `BIND(expr AS ?var)` form.
12///
13/// Rule bodies in Datalog¬ are conjunctions of literals. Positive
14/// literals must match facts in the database; negative literals
15/// must **not** match. Negation semantics follow the stratified
16/// negation-as-failure convention: every negated atom references
17/// only predicates in strictly lower strata, so the set of facts
18/// the negation is evaluated against is already closed by the time
19/// the rule fires.
20///
21/// `Filter` literals carry a [`BuiltinFilter`] expression that
22/// guards the current candidate binding — the evaluator drops a
23/// binding whenever the filter does not evaluate to `true`.
24///
25/// `Bind` literals carry a [`BindExpr`] expression and a fresh
26/// variable name; the evaluator extends the current candidate
27/// binding with `var → eval(expr, binding)` and the new variable
28/// is then visible to all subsequent body literals (including
29/// further `Bind`s, `Filter`s, the rule head, and SPARQL-style
30/// constraints over computed values). Filter and bind literals
31/// participate in safety analysis but contribute no edges to the
32/// predicate dependency graph used by stratification.
33#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
34#[serde(deny_unknown_fields)]
35pub enum Literal {
36    /// Positive literal — the body atom must match a fact.
37    Positive(Atom),
38    /// Negative literal — the body atom must **not** match any
39    /// fact.
40    Negative(Atom),
41    /// Built-in filter expression evaluated against the current
42    /// candidate binding.
43    Filter(BuiltinFilter),
44    /// `BIND(expr AS ?var)` — extend the current candidate
45    /// binding with a computed value. `var` must be fresh
46    /// (untouched by any earlier positive atom or `Bind`); every
47    /// variable inside `expr` must be bound by an earlier
48    /// positive atom or `Bind`.
49    Bind {
50        /// The fresh variable produced by this bind.
51        var: VariableSymbol,
52        /// The expression to compute. Evaluated against the
53        /// current candidate binding.
54        expr: BindExpr,
55    },
56    /// Aggregation sub-query — fold the inner body into a
57    /// per-group aggregate result that the outer body sees as a
58    /// freshly-bound variable. See [`AggregateBody`] for the
59    /// scoping and inner-body restrictions; the evaluator wiring
60    /// is a deferred extension follow-up.
61    Aggregate(AggregateBody),
62}
63
64impl Literal {
65    /// Access the underlying atom, when this literal carries one.
66    ///
67    /// Returns `None` for [`Literal::Filter`], [`Literal::Bind`],
68    /// and [`Literal::Aggregate`] — none of which carries a
69    /// single fact-matching atom in its top-level position.
70    /// (An aggregate's inner literals live inside
71    /// [`AggregateBody::inner_body`].)
72    #[must_use]
73    pub fn atom(&self) -> Option<&Atom> {
74        match self {
75            Self::Positive(atom) | Self::Negative(atom) => Some(atom),
76            Self::Filter(_) | Self::Bind { .. } | Self::Aggregate(_) => None,
77        }
78    }
79
80    /// True when the literal is positive.
81    #[must_use]
82    pub fn is_positive(&self) -> bool {
83        matches!(self, Self::Positive(_))
84    }
85
86    /// True when the literal is negated.
87    #[must_use]
88    pub fn is_negative(&self) -> bool {
89        matches!(self, Self::Negative(_))
90    }
91
92    /// True when the literal is a [`BuiltinFilter`] guard.
93    #[must_use]
94    pub fn is_filter(&self) -> bool {
95        matches!(self, Self::Filter(_))
96    }
97
98    /// True when the literal is a `BIND(expr AS ?var)` form.
99    #[must_use]
100    pub fn is_bind(&self) -> bool {
101        matches!(self, Self::Bind { .. })
102    }
103
104    /// True when the literal is an aggregation sub-query.
105    #[must_use]
106    pub fn is_aggregate(&self) -> bool {
107        matches!(self, Self::Aggregate(_))
108    }
109}