1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
//! Datalog [`Rule`] — head atom plus a conjunction of body
//! literals.
use std::collections::BTreeSet;
use super::aggregate::AggregateBody;
use super::atom::Atom;
use super::literal::Literal;
use super::symbols::VariableSymbol;
/// Datalog rule: `head :- body_1, ..., body_n`.
#[derive(Clone, Debug, Eq, Hash, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(try_from = "super::serde_input::RuleInput")]
pub struct Rule {
/// Head atom.
pub head: Atom,
/// Body literals. An empty body represents a fact.
pub body: Vec<Literal>,
}
impl Rule {
/// Construct a standalone rule only when it is safe and stratifiable.
/// # Errors
/// Rejects invalid terms, unsafe variables and recursion through negation.
pub fn try_new(head: Atom, body: Vec<Literal>) -> Result<Self, super::ProgramValidationError> {
let rule = Self { head, body };
// bounded: one authored rule, not a fact relation or graph.
super::Program {
rules: vec![rule.clone()],
}
.validate()?;
Ok(rule)
}
/// Construct a rule whose body contains only positive literals.
///
/// Convenience for the common case — callers that do not need
/// negation never have to name [`Literal`].
#[must_use]
pub fn positive(head: Atom, body: Vec<Atom>) -> Self {
Self {
head,
body: body.into_iter().map(Literal::Positive).collect(),
}
}
/// Construct a rule with an explicit literal body.
#[must_use]
pub fn new(head: Atom, body: Vec<Literal>) -> Self {
Self { head, body }
}
/// Returns true when the rule satisfies Datalog¬ safety.
///
/// A rule is safe when:
///
/// 1. Every variable appearing in the head is bound by some positive body
/// atom or `Bind`.
/// 2. Every variable appearing in a **negative** body literal is bound by
/// some positive body atom or `Bind` (range restriction —
/// negation-as-failure requires the negated atom to be ground by the
/// time it is evaluated).
/// 3. Every variable appearing inside a [`Literal::Filter`] is bound by
/// some positive body atom or `Bind` — filters guard candidate bindings,
/// they do not produce them.
/// 4. Every variable inside a [`Literal::Bind`] expression is bound by an
/// **earlier** positive body atom or `Bind`, and the `Bind`'s output
/// variable is fresh (not bound by any earlier positive atom or `Bind`).
#[must_use]
pub fn is_safe(&self) -> bool {
let unsafe_head = self.unsafe_head_variables();
let unsafe_neg = self.unsafe_negative_variables();
let unsafe_filter = self.unsafe_filter_variables();
let unsafe_bind = self.unsafe_bind_variables();
let unsafe_aggregate = self.unsafe_aggregate_variables();
unsafe_head.is_empty()
&& unsafe_neg.is_empty()
&& unsafe_filter.is_empty()
&& unsafe_bind.is_empty()
&& unsafe_aggregate.is_empty()
}
#[must_use]
pub(super) fn unsafe_head_variables(&self) -> Vec<VariableSymbol> {
let head_vars = self.head.variables();
let bound = self.bound_body_variables();
head_vars.difference(&bound).cloned().collect()
}
#[must_use]
pub(super) fn unsafe_negative_variables(&self) -> Vec<VariableSymbol> {
let bound = self.bound_body_variables();
self.body
.iter()
.filter_map(|lit| match lit {
Literal::Negative(atom) => Some(atom),
Literal::Positive(_)
| Literal::Filter(_)
| Literal::Bind { .. }
| Literal::Aggregate(_) => None,
})
.flat_map(Atom::variables)
.filter(|v| !bound.contains(v))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
#[must_use]
pub(super) fn unsafe_filter_variables(&self) -> Vec<VariableSymbol> {
let bound = self.bound_body_variables();
self.body
.iter()
.filter_map(|lit| match lit {
Literal::Filter(filter) => Some(filter.variables()),
Literal::Positive(_)
| Literal::Negative(_)
| Literal::Bind { .. }
| Literal::Aggregate(_) => None,
})
.flatten()
.filter(|v| !bound.contains(v))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
/// Position-aware safety check for `BIND(expr AS ?var)`.
///
/// Walks the body in order, building up the set of variables
/// bound by every positive atom and prior `Bind`. Each
/// `Bind` is inspected when first encountered:
///
/// * every variable inside `expr` must already be in the bound-set (i.e.
/// produced by an earlier positive atom or `Bind`);
/// * the `Bind`'s output variable must NOT already be in the bound-set —
/// `BIND` introduces *fresh* variables.
///
/// Both kinds of failure surface the offending variable for
/// diagnostics. Positive atoms anywhere in the body
/// contribute their variables to the bound-set up front (the
/// evaluator joins atoms in order, but positive-atom variables
/// are present at the binding-set level regardless of position
/// — so this only constrains the `Bind`-related ordering).
#[must_use]
pub(super) fn unsafe_bind_variables(&self) -> Vec<VariableSymbol> {
let mut bound = self.positive_atom_variables();
let mut errors: BTreeSet<VariableSymbol> = BTreeSet::new();
for lit in &self.body {
let Literal::Bind { var, expr } = lit else {
continue;
};
errors.extend(expr.variables().into_iter().filter(|v| !bound.contains(v)));
if bound.contains(var) {
errors.insert(var.clone());
}
bound.insert(var.clone());
}
errors.into_iter().collect()
}
/// Safety check for [`Literal::Aggregate`].
///
/// Each aggregate body literal must satisfy:
///
/// 1. `input_var` and every `group_by` key are bound by some inner-body
/// positive atom or `BIND` output (the aggregator can only fold values
/// that the inner sub-query actually produces).
/// 2. `result_var` is fresh — it does not appear in any other body
/// literal's variable set or in the rule's head before this aggregate
/// fires. (A later aggregate's `result_var` may shadow an earlier
/// aggregate's inner-only variable, since inner scopes don't leak.)
/// 3. Inner-body literals satisfy the same per-kind safety rules the outer
/// rule body enforces — negative literals and filters reference only
/// inner-positive / inner-`BIND` variables; inner `BIND` expressions
/// reference only earlier inner positives / binds and the `BIND` output
/// variable is fresh inside the inner scope.
///
/// Nested aggregates are reported separately by
/// [`Self::has_nested_aggregate`] and surface as
/// [`super::error::ProgramValidationError::NestedAggregateInInnerBody`].
#[must_use]
pub(super) fn unsafe_aggregate_variables(&self) -> Vec<VariableSymbol> {
let mut already_bound = self.positive_atom_variables();
for lit in &self.body {
if let Literal::Bind { var, .. } = lit {
already_bound.insert(var.clone());
}
}
let mut errors: BTreeSet<VariableSymbol> = BTreeSet::new();
for lit in &self.body {
let Literal::Aggregate(agg) = lit else {
continue;
};
let inner_bound = agg.inner_bound_variables();
if !inner_bound.contains(&agg.input_var) {
errors.insert(agg.input_var.clone());
}
for key in &agg.group_by {
if !inner_bound.contains(key) {
errors.insert(key.clone());
}
}
if already_bound.contains(&agg.result_var) {
errors.insert(agg.result_var.clone());
}
errors.extend(unsafe_inner_body_variables(agg, &inner_bound));
// Once this aggregate fires, its result_var becomes
// visible to subsequent body literals.
already_bound.insert(agg.result_var.clone());
}
errors.into_iter().collect()
}
/// Returns `true` when any aggregate body literal's `inner_body`
/// contains a nested [`Literal::Aggregate`]. Reported by
/// [`super::program::Program::validate`] as
/// [`super::error::ProgramValidationError::NestedAggregateInInnerBody`].
#[must_use]
pub(super) fn has_nested_aggregate_in_inner_body(&self) -> bool {
self.body.iter().any(|lit| match lit {
Literal::Aggregate(agg) => agg.contains_nested_aggregate(),
_ => false,
})
}
/// Variables bound by the time the rule head fires: positive
/// body atoms, every `Bind` output variable, and every
/// `Aggregate` body literal's exported variables
/// (`result_var` and `group_by` — the inner-only variables
/// stay scoped inside the aggregate). Used by head,
/// negation, and filter safety checks.
#[must_use]
fn bound_body_variables(&self) -> BTreeSet<VariableSymbol> {
let mut out = self.positive_atom_variables();
for lit in &self.body {
match lit {
Literal::Bind { var, .. } => {
out.insert(var.clone());
}
Literal::Aggregate(agg) => {
out.extend(agg.exported_variables());
}
Literal::Positive(_) | Literal::Negative(_) | Literal::Filter(_) => {}
}
}
out
}
/// Variables produced by positive body atoms only (no
/// `Bind` output variables, no aggregate exports). Used by
/// `Bind` safety as the initial bound-set before walking
/// the body.
#[must_use]
fn positive_atom_variables(&self) -> BTreeSet<VariableSymbol> {
self.body
.iter()
.filter_map(|lit| match lit {
Literal::Positive(atom) => Some(atom.variables()),
Literal::Negative(_)
| Literal::Filter(_)
| Literal::Bind { .. }
| Literal::Aggregate(_) => None,
})
.flatten()
.collect()
}
}
/// Validate variable safety inside an aggregate's inner body.
fn unsafe_inner_body_variables(
agg: &AggregateBody,
inner_bound: &BTreeSet<VariableSymbol>,
) -> BTreeSet<VariableSymbol> {
let mut errors: BTreeSet<VariableSymbol> = BTreeSet::new();
// Inner-Bind position-aware safety. Start the bound set
// from inner-positive variables; walk inner `BIND`s in
// order, checking expression references and freshness, and
// extending the bound set with each output.
let mut inner_bind_bound: BTreeSet<VariableSymbol> = agg
.inner_body
.iter()
.filter_map(|lit| match lit {
Literal::Positive(atom) => Some(atom.variables()),
_ => None,
})
.flatten()
.collect();
for lit in &agg.inner_body {
match lit {
Literal::Negative(atom) => {
for v in atom.variables() {
if !inner_bound.contains(&v) {
errors.insert(v);
}
}
}
Literal::Filter(filter) => {
for v in filter.variables() {
if !inner_bound.contains(&v) {
errors.insert(v);
}
}
}
Literal::Bind { var, expr } => {
for v in expr.variables() {
if !inner_bind_bound.contains(&v) {
errors.insert(v);
}
}
if inner_bind_bound.contains(var) {
errors.insert(var.clone());
}
inner_bind_bound.insert(var.clone());
}
Literal::Positive(_) | Literal::Aggregate(_) => {}
}
}
errors
}