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)]
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}