datalog_structural_model/ir/bind.rs
1//! `BIND(expr AS ?var)` expression tree.
2
3use std::collections::BTreeSet;
4
5use super::symbols::VariableSymbol;
6use super::term::Term;
7
8/// Expression carried by a
9/// [`Literal::Bind`](super::literal::Literal::Bind) body literal.
10///
11/// `BindExpr` covers identity assignment plus integer arithmetic
12/// over [`ConstantSymbol`](super::symbols::ConstantSymbol)s whose
13/// lexical form parses as an `i64`. Operands may be
14/// [`Term::Variable`]s (resolved against the current candidate
15/// binding) or [`Term::Constant`]s.
16///
17/// **Numeric semantics**: arithmetic operands must parse as
18/// base-10 `i64`; otherwise the expression is undefined for the
19/// current binding and the evaluator drops the candidate. SPARQL
20/// numeric promotion (integer → decimal → float → double),
21/// `xsd:decimal` arithmetic, and the remaining SPARQL string /
22/// date / IRI builtins inside `BIND` are tracked as extension
23/// follow-ups under
24/// the rule validation specification.
25///
26/// **String semantics**: `Str`, `StrLen`, `UCase`, `LCase`, and
27/// `Concat` operate on the lexical form of literals (when the
28/// operand classifies as a literal under the evaluator’s RDF term classification)
29/// or on the constant string itself otherwise. The result of
30/// `Str` / `UCase` / `LCase` / `Concat` is encoded as a plain
31/// literal (empty datatype IRI); the result of `StrLen` is an
32/// `xsd:integer` literal so subsequent arithmetic can consume it
33/// without re-parsing.
34#[derive(Clone, Debug, Eq, Hash, PartialEq)]
35pub enum BindExpr {
36 /// Identity — bind the variable to the term itself (typically
37 /// useful in the form `BIND(?x AS ?y)` for renaming).
38 Term(Term),
39 /// SPARQL `+`. Type-polymorphic in the forward engine: numeric
40 /// addition (over the integer → decimal → float → double
41 /// promotion lattice) or
42 /// `xsd:dateTime + xsd:dayTimeDuration → xsd:dateTime`.
43 /// Operand pairs that don't match either shape drop the
44 /// candidate.
45 Add(Box<BindExpr>, Box<BindExpr>),
46 /// SPARQL `-`. Type-polymorphic in the forward engine: numeric
47 /// subtraction, `xsd:dateTime - xsd:dayTimeDuration →
48 /// xsd:dateTime` (backward shift), or
49 /// `xsd:dateTime - xsd:dateTime → xsd:dayTimeDuration` (the
50 /// duration between two instants).
51 Sub(Box<BindExpr>, Box<BindExpr>),
52 /// Integer multiplication over `i64`.
53 Mul(Box<BindExpr>, Box<BindExpr>),
54 /// Integer division (truncated) over `i64`. Division by zero
55 /// is undefined and drops the candidate binding.
56 Div(Box<BindExpr>, Box<BindExpr>),
57 /// Integer negation over `i64`.
58 Neg(Box<BindExpr>),
59 /// `ABS(numeric)` — absolute value, preserving the input's SPARQL
60 /// numeric datatype (integer → integer, decimal → decimal,
61 /// float → float, double → double). A non-numeric operand drops
62 /// the candidate binding.
63 Abs(Box<BindExpr>),
64 /// `CEIL(numeric)` — smallest integer-valued result ≥ the input,
65 /// preserving the input's SPARQL numeric datatype. For an
66 /// `xsd:integer` input the value passes through unchanged. For
67 /// an `xsd:decimal` input the lexical form is the integer
68 /// ceiling (no fractional component) but the datatype stays
69 /// `xsd:decimal` per SPARQL 1.1 §17.4.4.4.
70 Ceil(Box<BindExpr>),
71 /// `FLOOR(numeric)` — largest integer-valued result ≤ the input,
72 /// preserving the input's SPARQL numeric datatype. Symmetric to
73 /// [`Self::Ceil`].
74 Floor(Box<BindExpr>),
75 /// `ROUND(numeric)` — round half toward positive infinity
76 /// (`XPath` `fn:round` semantics, matching SPARQL 1.1
77 /// §17.4.4.6). Preserves the input's SPARQL numeric datatype.
78 Round(Box<BindExpr>),
79 /// `STR(?x)` — lexical form. For literals returns a plain
80 /// literal carrying the lex; for IRIs / blank nodes returns
81 /// the constant string as a plain literal.
82 Str(Box<BindExpr>),
83 /// `STRLEN(?x)` — number of Unicode scalar values in the
84 /// lexical form. Result is an `xsd:integer` literal.
85 StrLen(Box<BindExpr>),
86 /// `UCASE(?x)` — uppercase the lexical form (Unicode-aware).
87 UCase(Box<BindExpr>),
88 /// `LCASE(?x)` — lowercase the lexical form (Unicode-aware).
89 LCase(Box<BindExpr>),
90 /// `CONCAT(?a, ?b, ...)` — concatenate the lexical forms of
91 /// each argument and return a plain literal.
92 Concat(Vec<BindExpr>),
93 /// `SUBSTR(value, start)` or `SUBSTR(value, start, len)` —
94 /// 1-based substring extraction over Unicode scalar values.
95 /// Negative or zero `start` clamps to position 1; start past
96 /// the end of the value yields the empty plain literal; an
97 /// over-long `len` is clamped to "until end". Mirrors the
98 /// SPARQL 1.1 semantics for `SUBSTR`.
99 SubStr {
100 /// The string-valued source expression.
101 value: Box<BindExpr>,
102 /// 1-based start position, in Unicode scalars.
103 start: Box<BindExpr>,
104 /// Optional length, in Unicode scalars. `None` means
105 /// "until end".
106 len: Option<Box<BindExpr>>,
107 },
108 /// `STRBEFORE(value, needle)` — substring of `value` before
109 /// the first occurrence of `needle`. Per SPARQL 1.1: empty
110 /// plain literal when `needle` is not found, and an empty
111 /// plain literal when `needle` itself is empty.
112 StrBefore(Box<BindExpr>, Box<BindExpr>),
113 /// `STRAFTER(value, needle)` — substring of `value` after
114 /// the first occurrence of `needle`. Per SPARQL 1.1: empty
115 /// plain literal when `needle` is not found, and the full
116 /// `value` when `needle` is empty.
117 StrAfter(Box<BindExpr>, Box<BindExpr>),
118 /// `REPLACE(value, pattern, replacement, flags?)` — replace
119 /// every match of the regex `pattern` (compiled with the
120 /// same SPARQL flag subset as
121 /// [`BuiltinFilter::RegexMatch`](crate::BuiltinFilter::RegexMatch)
122 /// — `i`, `m`, `s`, `x`) with `replacement`. A malformed
123 /// pattern, an unsupported flag, or any unbound variable
124 /// drops the candidate binding.
125 Replace {
126 /// The string-valued source expression.
127 value: Box<BindExpr>,
128 /// The regex pattern.
129 pattern: Box<BindExpr>,
130 /// The replacement template (back-references such as
131 /// `$1` follow the [`regex` crate]'s syntax).
132 ///
133 /// [`regex` crate]: https://docs.rs/regex
134 replacement: Box<BindExpr>,
135 /// Optional flags string (e.g. `"i"`, `"im"`).
136 flags: Option<Box<BindExpr>>,
137 },
138 /// `ENCODE_FOR_URI(value)` — percent-encode every code point
139 /// outside the RFC 3986 unreserved set (`ALPHA` / `DIGIT` /
140 /// `-` / `.` / `_` / `~`) into its UTF-8-byte `%HH` form.
141 EncodeForUri(Box<BindExpr>),
142 /// `MD5(value)` — RFC 1321 message digest of the lexical
143 /// form's UTF-8 bytes. Result is a plain literal carrying
144 /// the lowercase hex digest.
145 Md5(Box<BindExpr>),
146 /// `SHA1(value)` — FIPS 180-4 SHA-1 digest. Result is a
147 /// plain literal carrying the lowercase hex digest.
148 Sha1(Box<BindExpr>),
149 /// `SHA256(value)` — FIPS 180-4 SHA-256 digest.
150 Sha256(Box<BindExpr>),
151 /// `SHA384(value)` — FIPS 180-4 SHA-384 digest.
152 Sha384(Box<BindExpr>),
153 /// `SHA512(value)` — FIPS 180-4 SHA-512 digest.
154 Sha512(Box<BindExpr>),
155 /// `xsd:integer(value)` — parse the lexical form as base-10
156 /// `i64`. Emits an `xsd:integer` literal. A parse failure
157 /// drops the candidate binding.
158 CastInteger(Box<BindExpr>),
159 /// `xsd:decimal(value)` — parse the lexical form as a SPARQL
160 /// decimal (delegates to `bigdecimal::BigDecimal::from_str`).
161 /// Emits an `xsd:decimal` literal carrying the parsed
162 /// canonical form. A parse failure drops the candidate.
163 CastDecimal(Box<BindExpr>),
164 /// `xsd:double(value)` — parse the lexical form as a base-10
165 /// `f64`. Emits an `xsd:double` literal. A parse failure or
166 /// non-finite value drops the candidate.
167 CastDouble(Box<BindExpr>),
168 /// `xsd:float(value)` — parse the lexical form as a base-10
169 /// `f32`. Emits an `xsd:float` literal. A parse failure or
170 /// non-finite value drops the candidate.
171 CastFloat(Box<BindExpr>),
172 /// `xsd:string(value)` — emit the lexical form as an
173 /// `xsd:string`-typed literal. Always succeeds.
174 CastString(Box<BindExpr>),
175 /// `YEAR(dateTime)` — extract the year component as an
176 /// `xsd:integer` literal. The argument's lexical form must
177 /// parse as `xsd:dateTime`; otherwise the candidate drops.
178 Year(Box<BindExpr>),
179 /// `MONTH(dateTime)` — month component (1–12) as an
180 /// `xsd:integer` literal.
181 Month(Box<BindExpr>),
182 /// `DAY(dateTime)` — day-of-month (1–31) as an
183 /// `xsd:integer` literal.
184 Day(Box<BindExpr>),
185 /// `HOURS(dateTime)` — hour-of-day (0–23) as an
186 /// `xsd:integer` literal.
187 Hours(Box<BindExpr>),
188 /// `MINUTES(dateTime)` — minute-of-hour (0–59) as an
189 /// `xsd:integer` literal.
190 Minutes(Box<BindExpr>),
191 /// `SECONDS(dateTime)` — second-of-minute (including the
192 /// fractional part) as an `xsd:decimal` literal.
193 Seconds(Box<BindExpr>),
194 /// `TIMEZONE(dateTime)` — UTC offset as an
195 /// `xsd:dayTimeDuration` literal (e.g. `"PT5H"`, `"-PT5H"`,
196 /// `"PT0S"`). The candidate drops when the input has no
197 /// timezone designator (per SPARQL spec — `TIMEZONE` is an
198 /// error in that case).
199 Timezone(Box<BindExpr>),
200 /// `TZ(dateTime)` — UTC offset as an `xsd:string` literal:
201 /// `"Z"` for UTC, `"+05:00"` / `"-08:00"` etc. for offset
202 /// designators, and the empty string when the input has no
203 /// timezone.
204 Tz(Box<BindExpr>),
205 /// `NOW()` — current `xsd:dateTime` (UTC). The literal
206 /// returned is fixed at evaluation time; SPARQL guarantees
207 /// it stays constant across one query, but this Datalog
208 /// `BIND` evaluates the expression once per candidate
209 /// binding, which is consistent with SPARQL's stability
210 /// requirement (every reference under one rule firing sees
211 /// the same instant).
212 Now,
213 /// `LANG(?lit)` — extract the language tag from a
214 /// language-tagged literal. Returns a plain `xsd:string`
215 /// literal carrying the tag (lower-cased per SPARQL 1.1
216 /// canonicalization), or the empty string for non-language-
217 /// tagged literals. Drops the candidate when the operand is
218 /// not a literal (IRIs / blank nodes — SPARQL "type error
219 /// → no result").
220 Lang(Box<BindExpr>),
221 /// `IRI(?str)` — construct an IRI term from a string-valued
222 /// expression. If the operand is already an IRI it passes
223 /// through unchanged (SPARQL 1.1 §17.4.2.8). Drops the
224 /// candidate for blank-node operands.
225 Iri(Box<BindExpr>),
226 /// `BNODE(?seed)` — construct a blank node deterministically
227 /// from a string seed. Multiple calls with the same seed
228 /// yield the same blank-node label, which matches SPARQL 1.1
229 /// §17.4.2.4 "two calls with the same string return the same
230 /// blank node within one solution sequence" and gives rules
231 /// reproducible blank-node identity across firings. The
232 /// 0-arg `BNODE()` / `UUID()` / `STRUUID()` forms remain
233 /// non-deterministic and stay out of `BindExpr` (they require
234 /// the extension skolemization track).
235 BNode(Box<BindExpr>),
236}
237
238impl BindExpr {
239 /// All variables referenced anywhere inside the expression.
240 #[must_use]
241 pub fn variables(&self) -> BTreeSet<VariableSymbol> {
242 let mut out = BTreeSet::new();
243 self.collect_variables(&mut out);
244 out
245 }
246
247 fn collect_variables(&self, out: &mut BTreeSet<VariableSymbol>) {
248 match self {
249 Self::Term(term) => {
250 if let Some(name) = term.variable_name() {
251 out.insert(name.clone());
252 }
253 }
254 Self::Add(lhs, rhs)
255 | Self::Sub(lhs, rhs)
256 | Self::Mul(lhs, rhs)
257 | Self::Div(lhs, rhs) => {
258 lhs.collect_variables(out);
259 rhs.collect_variables(out);
260 }
261 Self::Neg(inner)
262 | Self::Abs(inner)
263 | Self::Ceil(inner)
264 | Self::Floor(inner)
265 | Self::Round(inner)
266 | Self::Str(inner)
267 | Self::StrLen(inner)
268 | Self::UCase(inner)
269 | Self::LCase(inner)
270 | Self::EncodeForUri(inner)
271 | Self::Md5(inner)
272 | Self::Sha1(inner)
273 | Self::Sha256(inner)
274 | Self::Sha384(inner)
275 | Self::Sha512(inner)
276 | Self::CastInteger(inner)
277 | Self::CastDecimal(inner)
278 | Self::CastDouble(inner)
279 | Self::CastFloat(inner)
280 | Self::CastString(inner)
281 | Self::Year(inner)
282 | Self::Month(inner)
283 | Self::Day(inner)
284 | Self::Hours(inner)
285 | Self::Minutes(inner)
286 | Self::Seconds(inner)
287 | Self::Timezone(inner)
288 | Self::Tz(inner)
289 | Self::Lang(inner)
290 | Self::Iri(inner)
291 | Self::BNode(inner) => inner.collect_variables(out),
292 Self::Now => {}
293 Self::Concat(parts) => {
294 for part in parts {
295 part.collect_variables(out);
296 }
297 }
298 Self::SubStr { value, start, len } => {
299 value.collect_variables(out);
300 start.collect_variables(out);
301 if let Some(len) = len {
302 len.collect_variables(out);
303 }
304 }
305 Self::StrBefore(value, needle) | Self::StrAfter(value, needle) => {
306 value.collect_variables(out);
307 needle.collect_variables(out);
308 }
309 Self::Replace {
310 value,
311 pattern,
312 replacement,
313 flags,
314 } => {
315 value.collect_variables(out);
316 pattern.collect_variables(out);
317 replacement.collect_variables(out);
318 if let Some(flags) = flags {
319 flags.collect_variables(out);
320 }
321 }
322 }
323 }
324}