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}