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}