Skip to main content

datalog_structural_model/ir/
filter.rs

1//! Built-in filter expressions guarding rule-body candidate
2//! bindings.
3
4use std::collections::BTreeSet;
5
6use super::symbols::VariableSymbol;
7use super::term::Term;
8
9/// Built-in filter expression.
10///
11/// Carries a tree of comparisons / string tests over Datalog
12/// [`Term`]s plus boolean compositors. The evaluator substitutes
13/// each variable with its bound constant from the current
14/// candidate binding and then evaluates the test; bindings that
15/// fail to substitute (variable not bound), encounter a malformed
16/// regex pattern, or evaluate to anything other than `true` are
17/// dropped from the frontier.
18///
19/// **Comparison semantics**: `Lt`, `Le`, `Gt`, `Ge` compare two
20/// [`ConstantSymbol`](super::symbols::ConstantSymbol)s
21/// lexicographically over the underlying string representation.
22/// SPARQL value-typed numeric or `xsd:dateTime` ordering is
23/// *not yet* implemented at this layer.
24///
25/// **String tests**: `StrStarts`, `StrEnds`, `Contains`, and
26/// `RegexMatch` operate on the raw constant string. SPARQL
27/// language-tag and datatype-aware semantics — `LANGMATCHES`,
28/// language-aware `REGEX` evaluation against `xsd:string`-only
29/// arguments, etc. — track as further extension follow-ups; see
30/// the rule validation specification.
31#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
32#[serde(deny_unknown_fields)]
33pub enum BuiltinFilter {
34    /// `lhs = rhs` — equality of constants.
35    Eq(Term, Term),
36    /// `lhs != rhs` — inequality of constants.
37    Ne(Term, Term),
38    /// `lhs < rhs` — lexicographic less-than over constants.
39    Lt(Term, Term),
40    /// `lhs <= rhs` — lexicographic less-than-or-equal.
41    Le(Term, Term),
42    /// `lhs > rhs` — lexicographic greater-than.
43    Gt(Term, Term),
44    /// `lhs >= rhs` — lexicographic greater-than-or-equal.
45    Ge(Term, Term),
46    /// `STRSTARTS(haystack, needle)` — `haystack` begins with
47    /// `needle` over the **lexical form** of each argument. Plain
48    /// literals (`"Alice"`), `xsd:string`-typed literals
49    /// (`"Alice"^^xsd:string`), and bare IRI constants all
50    /// compare against their bare-string representation, so the
51    /// match is insensitive to datatype framing.
52    StrStarts(Term, Term),
53    /// `STRENDS(haystack, needle)` — `haystack` ends with
54    /// `needle` over the lexical form of each argument. Same
55    /// datatype-insensitivity as [`Self::StrStarts`].
56    StrEnds(Term, Term),
57    /// `CONTAINS(haystack, needle)` — `haystack` contains
58    /// `needle` somewhere in the lexical form. Same
59    /// datatype-insensitivity as [`Self::StrStarts`].
60    Contains(Term, Term),
61    /// `REGEX(value, pattern, flags?)` — `value` matches the
62    /// `regex::Regex` compiled from `pattern` (and the optional
63    /// `flags`), comparing against `value`'s lexical form (same
64    /// datatype-insensitivity as [`Self::StrStarts`]). Supported
65    /// flags follow the SPARQL subset that maps to the
66    /// [`regex` crate]'s `(?…)` inline modifiers: `i`
67    /// (case-insensitive), `m` (multi-line), `s`
68    /// (dot-matches-newline), `x` (ignore whitespace inside the
69    /// pattern). A compile failure or unsupported flag is treated
70    /// as "undefined for the current binding" — the candidate is
71    /// dropped, matching the SPARQL "type error → not true"
72    /// framing.
73    ///
74    /// [`regex` crate]: https://docs.rs/regex
75    RegexMatch {
76        /// The string value to match against.
77        value: Term,
78        /// The regular expression pattern.
79        pattern: Term,
80        /// Optional flags string (e.g. `"i"`, `"im"`).
81        flags: Option<Term>,
82    },
83    /// `isIRI(?x)` — true when `?x` resolves to an IRI under the
84    /// term-kind classification in the evaluator’s RDF term classification.
85    IsIri(Term),
86    /// `isLiteral(?x)` — true when `?x` resolves to a literal.
87    IsLiteral(Term),
88    /// `isBlank(?x)` — true when `?x` resolves to a blank node.
89    IsBlank(Term),
90    /// `isNumeric(?x)` — true when `?x` resolves to a literal
91    /// whose datatype IRI is recognized as numeric by
92    /// the evaluator’s numeric datatype classification.
93    IsNumeric(Term),
94    /// `LANGMATCHES(language-tag, language-range)` — RFC 4647
95    /// basic-range matching over the lexical form of both
96    /// arguments. The wildcard range `"*"` matches any non-empty
97    /// tag; otherwise the range matches the tag when the tag's
98    /// initial subtags (split on `-`) match the range's subtags
99    /// case-insensitively.
100    ///
101    /// Both arguments are resolved through the `lexical_form`
102    /// helper, so callers may supply either a plain string
103    /// constant or an encoded literal (typically the result of a
104    /// future `BindExpr::Lang(?lit)` extraction). The full
105    /// pipeline integration that wires `LANG(?lit)` through to
106    /// language-tagged literal storage is tracked under R12-21
107    /// follow-up — this slice lands the rule and the basic-range
108    /// matcher; existing tests construct the language tag as a
109    /// plain literal manually.
110    LangMatches(Term, Term),
111    /// `lhs && rhs` — logical conjunction of two filters.
112    And(Box<BuiltinFilter>, Box<BuiltinFilter>),
113    /// `lhs || rhs` — logical disjunction of two filters.
114    Or(Box<BuiltinFilter>, Box<BuiltinFilter>),
115    /// `!inner` — logical negation.
116    Not(Box<BuiltinFilter>),
117}
118
119impl BuiltinFilter {
120    /// All variables referenced anywhere inside the filter
121    /// expression. Used by safety analysis to require that every
122    /// such variable is bound by some positive body atom.
123    #[must_use]
124    pub fn variables(&self) -> BTreeSet<VariableSymbol> {
125        let mut out = BTreeSet::new();
126        self.collect_variables(&mut out);
127        out
128    }
129
130    fn collect_variables(&self, out: &mut BTreeSet<VariableSymbol>) {
131        match self {
132            Self::Eq(a, b)
133            | Self::Ne(a, b)
134            | Self::Lt(a, b)
135            | Self::Le(a, b)
136            | Self::Gt(a, b)
137            | Self::Ge(a, b)
138            | Self::StrStarts(a, b)
139            | Self::StrEnds(a, b)
140            | Self::Contains(a, b) => {
141                insert_variable(a, out);
142                insert_variable(b, out);
143            }
144            Self::RegexMatch {
145                value,
146                pattern,
147                flags,
148            } => {
149                insert_variable(value, out);
150                insert_variable(pattern, out);
151                if let Some(flags) = flags {
152                    insert_variable(flags, out);
153                }
154            }
155            Self::IsIri(t) | Self::IsLiteral(t) | Self::IsBlank(t) | Self::IsNumeric(t) => {
156                insert_variable(t, out);
157            }
158            Self::LangMatches(tag, range) => {
159                insert_variable(tag, out);
160                insert_variable(range, out);
161            }
162            Self::And(lhs, rhs) | Self::Or(lhs, rhs) => {
163                lhs.collect_variables(out);
164                rhs.collect_variables(out);
165            }
166            Self::Not(inner) => inner.collect_variables(out),
167        }
168    }
169}
170
171fn insert_variable(term: &Term, out: &mut BTreeSet<VariableSymbol>) {
172    term.collect_variables(out);
173}