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
//! [`ProgramValidationError`] and its `Display` / `Error` /
//! `From` integration with [`StratificationError`].
use std::error::Error;
use std::fmt;
use super::symbols::{PredicateSymbol, VariableSymbol};
use crate::stratify::StratificationError;
/// Program validation failures.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub enum ProgramValidationError {
/// Rule is unsafe because one or more head variables are
/// missing from the positive body literals.
UnsafeRule {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
/// Missing variables.
variables: Vec<VariableSymbol>,
},
/// Rule has a negated body literal whose variables are not
/// bound by any positive body literal (range-restriction
/// violation — negation-as-failure requires the negated atom
/// to be ground at evaluation time).
UnsafeNegation {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
/// Unbound variables in negated literals.
variables: Vec<VariableSymbol>,
},
/// Rule has a [`Literal::Filter`](super::literal::Literal::Filter)
/// referencing variables that no positive body literal binds.
/// Filters guard candidate bindings; they cannot introduce
/// new ones.
UnsafeFilter {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
/// Unbound variables in filter expressions.
variables: Vec<VariableSymbol>,
},
/// Rule has a [`Literal::Bind`](super::literal::Literal::Bind)
/// whose `expr` references a variable not bound by an earlier
/// positive atom or `Bind`, or whose output `var` shadows a
/// variable that is already bound by an earlier positive atom
/// or `Bind`. Both cases produce undefined evaluation order
/// under sequential rule-body semantics.
UnsafeBind {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
/// Offending variables — either an unbound expression
/// reference or a colliding output name.
variables: Vec<VariableSymbol>,
},
/// Rule has a
/// [`Literal::Aggregate`](super::literal::Literal::Aggregate)
/// that violates aggregation safety:
///
/// - `input_var` or a `group_by` key is not bound by the inner body.
/// - `result_var` is not fresh (collides with an outer-rule binding).
/// - An inner negative literal or filter references a variable that no
/// inner positive atom or `BIND` produces.
/// - An inner `BIND` references a variable not bound by an earlier inner
/// positive atom or `BIND`, or its output variable collides with an
/// existing inner-scope binding.
///
/// Nested aggregates inside an aggregate's inner body surface
/// separately as [`Self::NestedAggregateInInnerBody`].
UnsafeAggregate {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
/// Offending variables.
variables: Vec<VariableSymbol>,
},
/// Rule has a [`Literal::Aggregate`](super::literal::Literal::Aggregate)
/// whose `inner_body` itself contains a nested
/// `Literal::Aggregate`. Nested aggregation is out of scope —
/// the evaluator only folds a flat inner sub-query — and the
/// inner aggregate's stratification dependencies would
/// require an extra rewriting pass. Authors that need
/// aggregation over an aggregate's output should lift the
/// inner aggregate into its own intermediate rule whose
/// head feeds the outer aggregate's `inner_body`.
NestedAggregateInInnerBody {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
},
/// Program contains a recursive cycle through a negation
/// edge. Such programs have no unique least fixed point under
/// stratified negation-as-failure semantics.
UnstratifiableNegation {
/// Predicate symbols whose stratification could not be
/// resolved. Includes every predicate in the offending
/// strongly connected component.
predicates: Vec<PredicateSymbol>,
},
/// Rule contains a [`Literal::Negative`](super::literal::Literal::Negative)
/// while equality mode is
/// [`EqualityMode::SameAs`](super::EqualityMode::SameAs).
///
/// Under equality-driven rewriting, terms get rewritten across
/// equality classes, which interacts
/// non-deterministically with negation-as-failure (the set
/// the negated atom is evaluated against changes shape as
/// equality classes merge). Programs that combine negation with
/// equality mode are rejected for this reason. This
/// crate enforces the same restriction so semantics stay stable
/// under the equality rewrite.
NegationUnderEqualityMode {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
},
/// Rule contains a
/// [`Literal::Aggregate`](super::literal::Literal::Aggregate)
/// while equality mode is
/// [`EqualityMode::SameAs`](super::EqualityMode::SameAs).
///
/// Aggregation depends on the *complete* extension of its
/// inner predicates. Under equality mode that extension is
/// non-monotonic as equality classes merge term identities,
/// which would silently change aggregate results across
/// reasoning rounds. This crate rejects the combination.
AggregationUnderEqualityMode {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
},
/// Rule has a body atom (positive, negative, filter, bind, or
/// aggregate inner atom) that carries a
/// [`Term::Skolem`](super::term::Term::Skolem) term. SKOLEM is
/// a deterministic IRI-minting function and is only
/// well-defined in head positions — using it as a body pattern
/// would be a no-op match (the freshly-minted IRI never unifies
/// with anything in the EDB). See extension.
SkolemInBodyAtom {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
},
/// Rule contains a
/// [`Term::TripleTerm`](super::term::Term::TripleTerm) nested
/// deeper than
/// [`MAX_TRIPLE_TERM_NESTING`](super::term::MAX_TRIPLE_TERM_NESTING)
/// in either the head or a body atom. Unification cost grows
/// with nesting; the bound exists to keep evaluation finite.
TripleTermNestingExceeded {
/// Rule index in [`Program::rules`](super::program::Program::rules).
rule_index: usize,
/// Observed nesting depth (always >
/// `MAX_TRIPLE_TERM_NESTING`).
depth: usize,
/// Configured limit at the time of validation.
limit: usize,
},
/// A Skolem argument is neither a variable nor a constant.
InvalidSkolemArguments {
/// Rule containing the invalid term.
rule_index: usize,
},
}
impl From<StratificationError> for ProgramValidationError {
fn from(err: StratificationError) -> Self {
let StratificationError::UnstratifiableCycle { predicates } = err;
Self::UnstratifiableNegation { predicates }
}
}
impl fmt::Display for ProgramValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsafeRule {
rule_index,
variables,
} => {
write!(
f,
"rule at index {rule_index} is unsafe; missing head variables in body: "
)?;
for (idx, var) in variables.iter().enumerate() {
if idx > 0 {
write!(f, ", ")?;
}
write!(f, "{}", var.as_str())?;
}
Ok(())
}
Self::UnsafeNegation {
rule_index,
variables,
} => {
write!(
f,
"rule at index {rule_index} has unsafe negation; variables in negated \
literals are not bound by positive body: "
)?;
for (idx, var) in variables.iter().enumerate() {
if idx > 0 {
write!(f, ", ")?;
}
write!(f, "{}", var.as_str())?;
}
Ok(())
}
Self::UnsafeFilter {
rule_index,
variables,
} => {
write!(
f,
"rule at index {rule_index} has an unsafe filter; variables in filter \
expressions are not bound by positive body: "
)?;
for (idx, var) in variables.iter().enumerate() {
if idx > 0 {
write!(f, ", ")?;
}
write!(f, "{}", var.as_str())?;
}
Ok(())
}
Self::UnsafeBind {
rule_index,
variables,
} => {
write!(
f,
"rule at index {rule_index} has an unsafe BIND; offending variables \
(unbound expr reference or colliding output name): "
)?;
for (idx, var) in variables.iter().enumerate() {
if idx > 0 {
write!(f, ", ")?;
}
write!(f, "{}", var.as_str())?;
}
Ok(())
}
Self::UnsafeAggregate {
rule_index,
variables,
} => {
write!(
f,
"rule at index {rule_index} has an unsafe aggregate; offending variables \
(unbound input / group-by, colliding result_var, or unsupported inner \
literal): "
)?;
for (idx, var) in variables.iter().enumerate() {
if idx > 0 {
write!(f, ", ")?;
}
write!(f, "{}", var.as_str())?;
}
Ok(())
}
Self::NestedAggregateInInnerBody { rule_index } => {
write!(
f,
"rule at index {rule_index} has an aggregate whose inner body contains a \
nested aggregate; nested aggregation is not supported — lift the inner \
aggregate into its own rule"
)
}
Self::NegationUnderEqualityMode { rule_index } => {
write!(
f,
"rule at index {rule_index} contains a negative literal while equality mode \
is `SameAs`; negation and equality rewriting are mutually \
exclusive (extension)"
)
}
Self::AggregationUnderEqualityMode { rule_index } => {
write!(
f,
"rule at index {rule_index} contains an aggregate body literal while equality \
mode is `SameAs`; aggregation and equality rewriting are \
mutually exclusive (extension)"
)
}
Self::UnstratifiableNegation { predicates } => {
write!(f, "program contains a recursive cycle through negation: ")?;
for (idx, p) in predicates.iter().enumerate() {
if idx > 0 {
write!(f, ", ")?;
}
write!(f, "{}", p.as_str())?;
}
Ok(())
}
Self::SkolemInBodyAtom { rule_index } => {
write!(
f,
"rule at index {rule_index} has a body atom carrying a SKOLEM term; \
SKOLEM is only allowed in head atoms (extension)"
)
}
Self::InvalidSkolemArguments { rule_index } => write!(
f,
"rule at index {rule_index} has non-flat Skolem arguments"
),
Self::TripleTermNestingExceeded {
rule_index,
depth,
limit,
} => {
write!(
f,
"rule at index {rule_index} contains a triple-term nested {depth} deep; \
limit is {limit}"
)
}
}
}
}
impl Error for ProgramValidationError {}