Skip to main content

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}