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)]
34pub enum Literal {
35    /// Positive literal — the body atom must match a fact.
36    Positive(Atom),
37    /// Negative literal — the body atom must **not** match any
38    /// fact.
39    Negative(Atom),
40    /// Built-in filter expression evaluated against the current
41    /// candidate binding.
42    Filter(BuiltinFilter),
43    /// `BIND(expr AS ?var)` — extend the current candidate
44    /// binding with a computed value. `var` must be fresh
45    /// (untouched by any earlier positive atom or `Bind`); every
46    /// variable inside `expr` must be bound by an earlier
47    /// positive atom or `Bind`.
48    Bind {
49        /// The fresh variable produced by this bind.
50        var: VariableSymbol,
51        /// The expression to compute. Evaluated against the
52        /// current candidate binding.
53        expr: BindExpr,
54    },
55    /// Aggregation sub-query — fold the inner body into a
56    /// per-group aggregate result that the outer body sees as a
57    /// freshly-bound variable. See [`AggregateBody`] for the
58    /// scoping and inner-body restrictions; the evaluator wiring
59    /// is a deferred extension follow-up.
60    Aggregate(AggregateBody),
61}
62
63impl Literal {
64    /// Access the underlying atom, when this literal carries one.
65    ///
66    /// Returns `None` for [`Literal::Filter`], [`Literal::Bind`],
67    /// and [`Literal::Aggregate`] — none of which carries a
68    /// single fact-matching atom in its top-level position.
69    /// (An aggregate's inner literals live inside
70    /// [`AggregateBody::inner_body`].)
71    #[must_use]
72    pub fn atom(&self) -> Option<&Atom> {
73        match self {
74            Self::Positive(atom) | Self::Negative(atom) => Some(atom),
75            Self::Filter(_) | Self::Bind { .. } | Self::Aggregate(_) => None,
76        }
77    }
78
79    /// True when the literal is positive.
80    #[must_use]
81    pub fn is_positive(&self) -> bool {
82        matches!(self, Self::Positive(_))
83    }
84
85    /// True when the literal is negated.
86    #[must_use]
87    pub fn is_negative(&self) -> bool {
88        matches!(self, Self::Negative(_))
89    }
90
91    /// True when the literal is a [`BuiltinFilter`] guard.
92    #[must_use]
93    pub fn is_filter(&self) -> bool {
94        matches!(self, Self::Filter(_))
95    }
96
97    /// True when the literal is a `BIND(expr AS ?var)` form.
98    #[must_use]
99    pub fn is_bind(&self) -> bool {
100        matches!(self, Self::Bind { .. })
101    }
102
103    /// True when the literal is an aggregation sub-query.
104    #[must_use]
105    pub fn is_aggregate(&self) -> bool {
106        matches!(self, Self::Aggregate(_))
107    }
108}