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