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