Skip to main content

qualia_core_db/modalities/logic/owl/
materialize.rs

1//! OWL 2 RL forward-chaining materialization over NQuin-style triples.
2//!
3//! This is the *reasoner* companion to [`super::shacl_convert`] (which only
4//! *lowers* OWL vocabularies into SHACL shapes). Here we compute the OWL 2 RL
5//! entailment closure of a triple set by datalog-style fixpoint iteration — the
6//! standard PTIME approach to OWL 2 RL — using only fixed caller-supplied buffers
7//! (zero heap; no `Vec`/`Box`). Three audit capabilities live here:
8//!
9//! 1. **OWL 2 RL partial materialization** — the property/class-axiom rule subset
10//!    that needs no RDF-list decoding: `cax-sco`, `prp-spo1`, `prp-dom`, `prp-rng`,
11//!    `prp-symp`, `prp-trp`, `prp-inv`, `prp-fp`, `prp-ifp`, `eq-sym`, `eq-trans`,
12//!    `scm-sco`, `scm-spo`, plus equivalence expansion (`scm-eqc`/`scm-eqp`),
13//!    iterated to a fixpoint. This is the polynomial-time core of OWL 2 RL.
14//! 2. **Disjointness contradiction isolation** (`cax-dw`) — a violating individual
15//!    is recorded into a quarantine buffer and the closure *keeps going*. An
16//!    inconsistency does NOT explode the graph into "everything entailed"; the rest
17//!    of the Information-Banking ecosystem stays usable off-grid.
18//! 3. **Property-chain axiom unrolling** — `p ⊑ p1 ∘ p2` is composed by the sparse
19//!    boolean-relation product (the join form of a boolean matrix multiply),
20//!    supplied as explicit [`ChainAxiom`]s (the internal form an N3/OWL parser
21//!    produces from `owl:propertyChainAxiom (p1 p2)`).
22//!
23//! ## Complexity
24//! Each fixpoint pass scans a stable prefix of the bounded working set and inserts
25//! deduplicated derivations into the tail; iteration stops at a fixpoint or a caller
26//! `max_iters` cap. With a working set bounded by the caller's buffer this is
27//! polynomial and terminates — suitable for constrained hardware.
28
29use crate::NQuin;
30
31// ── OWL / RDFS vocabulary (FNV-1a hashed at compile time via `q_hash`) ──────────
32
33pub const RDF_TYPE: u64 = crate::q_hash("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
34pub const RDFS_SUBCLASS_OF: u64 = crate::q_hash("http://www.w3.org/2000/01/rdf-schema#subClassOf");
35pub const RDFS_SUBPROPERTY_OF: u64 =
36    crate::q_hash("http://www.w3.org/2000/01/rdf-schema#subPropertyOf");
37pub const RDFS_DOMAIN: u64 = crate::q_hash("http://www.w3.org/2000/01/rdf-schema#domain");
38pub const RDFS_RANGE: u64 = crate::q_hash("http://www.w3.org/2000/01/rdf-schema#range");
39pub const OWL_SAME_AS: u64 = crate::q_hash("http://www.w3.org/2002/07/owl#sameAs");
40pub const OWL_INVERSE_OF: u64 = crate::q_hash("http://www.w3.org/2002/07/owl#inverseOf");
41pub const OWL_EQUIVALENT_CLASS: u64 =
42    crate::q_hash("http://www.w3.org/2002/07/owl#equivalentClass");
43pub const OWL_EQUIVALENT_PROPERTY: u64 =
44    crate::q_hash("http://www.w3.org/2002/07/owl#equivalentProperty");
45pub const OWL_DISJOINT_WITH: u64 = crate::q_hash("http://www.w3.org/2002/07/owl#disjointWith");
46pub const OWL_SYMMETRIC_PROPERTY: u64 =
47    crate::q_hash("http://www.w3.org/2002/07/owl#SymmetricProperty");
48pub const OWL_TRANSITIVE_PROPERTY: u64 =
49    crate::q_hash("http://www.w3.org/2002/07/owl#TransitiveProperty");
50pub const OWL_FUNCTIONAL_PROPERTY: u64 =
51    crate::q_hash("http://www.w3.org/2002/07/owl#FunctionalProperty");
52pub const OWL_INVERSE_FUNCTIONAL_PROPERTY: u64 =
53    crate::q_hash("http://www.w3.org/2002/07/owl#InverseFunctionalProperty");
54
55// ── Types ───────────────────────────────────────────────────────────────────
56
57/// A reasoning triple — the `(subject, predicate, object)` projection of an NQuin.
58/// `Copy` and 24 bytes, so working sets live entirely in caller stack/arena buffers.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct RdfTriple {
61    pub s: u64,
62    pub p: u64,
63    pub o: u64,
64}
65
66impl RdfTriple {
67    pub const fn new(s: u64, p: u64, o: u64) -> Self {
68        Self { s, p, o }
69    }
70
71    /// Project an `NQuin` to its `(subject, predicate, object)` reasoning triple.
72    pub const fn from_nquin(q: &NQuin) -> Self {
73        Self {
74            s: q.subject,
75            p: q.predicate,
76            o: q.object,
77        }
78    }
79}
80
81/// A 2-step property chain axiom `composed ⊑ first ∘ second`. An OWL/N3 parser
82/// lowers `composed owl:propertyChainAxiom (first second)` into this form.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct ChainAxiom {
85    pub composed: u64,
86    pub first: u64,
87    pub second: u64,
88}
89
90/// A recorded `cax-dw` disjointness contradiction: `individual` was inferred to be
91/// a member of two `owl:disjointWith` classes. Reported, not fatal.
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct DisjointnessViolation {
94    pub individual: u64,
95    pub class_a: u64,
96    pub class_b: u64,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum MaterializeError {
101    /// The working-set buffer filled before the closure saturated. Grow `triples`.
102    WorkingSetFull,
103    /// The contradiction buffer filled. Grow `contradictions_out`.
104    ContradictionBufferFull,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct MaterializeSummary {
109    /// Total triples after materialization (inputs + inferred).
110    pub triple_count: usize,
111    /// Newly inferred triples (`triple_count - initial_len`).
112    pub inferred_count: usize,
113    /// Disjointness contradictions quarantined.
114    pub contradiction_count: usize,
115    /// Fixpoint passes performed.
116    pub iterations: u32,
117    /// `true` if a fixpoint was reached (no new triples), `false` if `max_iters` hit.
118    pub saturated: bool,
119}
120
121// ── Working-set helpers ───────────────────────────────────────────────────────
122
123#[inline]
124fn contains(triples: &[RdfTriple], len: usize, t: RdfTriple) -> bool {
125    triples[..len].iter().any(|&x| x == t)
126}
127
128/// Insert `t` if absent. Returns `Ok(true)` if newly inserted, `Ok(false)` if a
129/// duplicate, `Err(WorkingSetFull)` if the buffer is full.
130#[inline]
131fn try_push(
132    triples: &mut [RdfTriple],
133    len: &mut usize,
134    t: RdfTriple,
135) -> Result<bool, MaterializeError> {
136    if contains(triples, *len, t) {
137        return Ok(false);
138    }
139    if *len >= triples.len() {
140        return Err(MaterializeError::WorkingSetFull);
141    }
142    triples[*len] = t;
143    *len += 1;
144    Ok(true)
145}
146
147// ── Materialization ───────────────────────────────────────────────────────────
148
149/// Compute the OWL 2 RL entailment closure of `triples[..initial_len]` in place.
150///
151/// `triples` must be sized to hold inputs *plus* derivations; the function fills
152/// the tail and returns the new total length in the summary. `chains` supplies
153/// property-chain axioms. Disjointness contradictions are written to
154/// `contradictions_out`. The closure isolates contradictions (records and
155/// continues) rather than halting.
156pub fn materialize_owl_rl(
157    triples: &mut [RdfTriple],
158    initial_len: usize,
159    chains: &[ChainAxiom],
160    max_iters: u32,
161    contradictions_out: &mut [DisjointnessViolation],
162) -> Result<MaterializeSummary, MaterializeError> {
163    let mut len = initial_len.min(triples.len());
164    let mut iterations = 0u32;
165    let mut saturated = false;
166
167    while iterations < max_iters {
168        iterations += 1;
169        let mut changed = false;
170        let n = len; // stable antecedent prefix for this pass
171
172        for i in 0..n {
173            let t = triples[i];
174
175            // scm-eqc / scm-eqp: equivalence expands to both-way sub-axioms.
176            if t.p == OWL_EQUIVALENT_CLASS {
177                changed |= try_push(
178                    triples,
179                    &mut len,
180                    RdfTriple::new(t.s, RDFS_SUBCLASS_OF, t.o),
181                )?;
182                changed |= try_push(
183                    triples,
184                    &mut len,
185                    RdfTriple::new(t.o, RDFS_SUBCLASS_OF, t.s),
186                )?;
187            } else if t.p == OWL_EQUIVALENT_PROPERTY {
188                changed |= try_push(
189                    triples,
190                    &mut len,
191                    RdfTriple::new(t.s, RDFS_SUBPROPERTY_OF, t.o),
192                )?;
193                changed |= try_push(
194                    triples,
195                    &mut len,
196                    RdfTriple::new(t.o, RDFS_SUBPROPERTY_OF, t.s),
197                )?;
198            }
199            // eq-sym: sameAs is symmetric.
200            else if t.p == OWL_SAME_AS && t.s != t.o {
201                changed |= try_push(triples, &mut len, RdfTriple::new(t.o, OWL_SAME_AS, t.s))?;
202            }
203            // prp-inv: x p1 y ⟹ y p2 x for each (p1 inverseOf p2). Walk inverse decls.
204            else if t.p == OWL_INVERSE_OF {
205                let (p1, p2) = (t.s, t.o);
206                for j in 0..n {
207                    let u = triples[j];
208                    if u.p == p1 {
209                        changed |= try_push(triples, &mut len, RdfTriple::new(u.o, p2, u.s))?;
210                    }
211                    if u.p == p2 {
212                        changed |= try_push(triples, &mut len, RdfTriple::new(u.o, p1, u.s))?;
213                    }
214                }
215            }
216        }
217
218        // scm-sco / scm-spo: subClassOf / subPropertyOf transitivity.
219        for i in 0..n {
220            let a = triples[i];
221            if a.p != RDFS_SUBCLASS_OF && a.p != RDFS_SUBPROPERTY_OF {
222                continue;
223            }
224            for j in 0..n {
225                let b = triples[j];
226                if b.p == a.p && b.s == a.o && a.s != b.o {
227                    changed |= try_push(triples, &mut len, RdfTriple::new(a.s, a.p, b.o))?;
228                }
229            }
230        }
231
232        // cax-sco: x type c1 ∧ c1 subClassOf c2 ⟹ x type c2.
233        // prp-spo1: x p1 y ∧ p1 subPropertyOf p2 ⟹ x p2 y.
234        for i in 0..n {
235            let inst = triples[i];
236            for j in 0..n {
237                let ax = triples[j];
238                if ax.p == RDFS_SUBCLASS_OF
239                    && inst.p == RDF_TYPE
240                    && inst.o == ax.s
241                    && inst.o != ax.o
242                {
243                    changed |= try_push(triples, &mut len, RdfTriple::new(inst.s, RDF_TYPE, ax.o))?;
244                } else if ax.p == RDFS_SUBPROPERTY_OF && inst.p == ax.s && ax.s != ax.o {
245                    changed |= try_push(triples, &mut len, RdfTriple::new(inst.s, ax.o, inst.o))?;
246                }
247            }
248        }
249
250        // prp-dom / prp-rng: property domain/range typing.
251        for i in 0..n {
252            let ax = triples[i];
253            if ax.p != RDFS_DOMAIN && ax.p != RDFS_RANGE {
254                continue;
255            }
256            for j in 0..n {
257                let inst = triples[j];
258                if inst.p == ax.s {
259                    let subj = if ax.p == RDFS_DOMAIN { inst.s } else { inst.o };
260                    changed |= try_push(triples, &mut len, RdfTriple::new(subj, RDF_TYPE, ax.o))?;
261                }
262            }
263        }
264
265        // prp-symp: p type SymmetricProperty ∧ x p y ⟹ y p x.
266        // prp-trp:  p type TransitiveProperty ∧ x p y ∧ y p z ⟹ x p z.
267        for i in 0..n {
268            let inst = triples[i];
269            if inst.p == RDF_TYPE {
270                continue;
271            }
272            let is_symmetric = contains(
273                triples,
274                len,
275                RdfTriple::new(inst.p, RDF_TYPE, OWL_SYMMETRIC_PROPERTY),
276            );
277            if is_symmetric && inst.s != inst.o {
278                changed |= try_push(triples, &mut len, RdfTriple::new(inst.o, inst.p, inst.s))?;
279            }
280            let is_transitive = contains(
281                triples,
282                len,
283                RdfTriple::new(inst.p, RDF_TYPE, OWL_TRANSITIVE_PROPERTY),
284            );
285            if is_transitive {
286                for j in 0..n {
287                    let next = triples[j];
288                    if next.p == inst.p && next.s == inst.o && inst.s != next.o {
289                        changed |=
290                            try_push(triples, &mut len, RdfTriple::new(inst.s, inst.p, next.o))?;
291                    }
292                }
293            }
294        }
295
296        // prp-fp:  p type FunctionalProperty ∧ x p y1 ∧ x p y2 ⟹ y1 sameAs y2.
297        // prp-ifp: p type InverseFunctionalProperty ∧ x1 p y ∧ x2 p y ⟹ x1 sameAs x2.
298        for i in 0..n {
299            let a = triples[i];
300            if a.p == RDF_TYPE {
301                continue;
302            }
303            let functional = contains(
304                triples,
305                len,
306                RdfTriple::new(a.p, RDF_TYPE, OWL_FUNCTIONAL_PROPERTY),
307            );
308            let inv_functional = contains(
309                triples,
310                len,
311                RdfTriple::new(a.p, RDF_TYPE, OWL_INVERSE_FUNCTIONAL_PROPERTY),
312            );
313            if !functional && !inv_functional {
314                continue;
315            }
316            for j in 0..n {
317                let b = triples[j];
318                if b.p != a.p {
319                    continue;
320                }
321                if functional && b.s == a.s && a.o != b.o {
322                    changed |= try_push(triples, &mut len, RdfTriple::new(a.o, OWL_SAME_AS, b.o))?;
323                }
324                if inv_functional && b.o == a.o && a.s != b.s {
325                    changed |= try_push(triples, &mut len, RdfTriple::new(a.s, OWL_SAME_AS, b.s))?;
326                }
327            }
328        }
329
330        // eq-trans: sameAs transitivity.
331        for i in 0..n {
332            let a = triples[i];
333            if a.p != OWL_SAME_AS {
334                continue;
335            }
336            for j in 0..n {
337                let b = triples[j];
338                if b.p == OWL_SAME_AS && b.s == a.o && a.s != b.o {
339                    changed |= try_push(triples, &mut len, RdfTriple::new(a.s, OWL_SAME_AS, b.o))?;
340                }
341            }
342        }
343
344        // Property-chain unrolling: composed ⊑ first ∘ second (sparse boolean product).
345        for chain in chains {
346            for i in 0..n {
347                let lhs = triples[i];
348                if lhs.p != chain.first {
349                    continue;
350                }
351                for j in 0..n {
352                    let rhs = triples[j];
353                    if rhs.p == chain.second && rhs.s == lhs.o {
354                        changed |= try_push(
355                            triples,
356                            &mut len,
357                            RdfTriple::new(lhs.s, chain.composed, rhs.o),
358                        )?;
359                    }
360                }
361            }
362        }
363
364        if !changed {
365            saturated = true;
366            break;
367        }
368    }
369
370    // ── cax-dw: disjointness contradiction isolation (post-closure) ──────────
371    let mut contradiction_count = 0usize;
372    for i in 0..len {
373        let dj = triples[i];
374        if dj.p != OWL_DISJOINT_WITH {
375            continue;
376        }
377        let (c1, c2) = (dj.s, dj.o);
378        for j in 0..len {
379            let tj = triples[j];
380            if tj.p != RDF_TYPE || tj.o != c1 {
381                continue;
382            }
383            let x = tj.s;
384            if !contains(triples, len, RdfTriple::new(x, RDF_TYPE, c2)) {
385                continue;
386            }
387            let dup = contradictions_out[..contradiction_count].iter().any(|e| {
388                e.individual == x
389                    && ((e.class_a == c1 && e.class_b == c2)
390                        || (e.class_a == c2 && e.class_b == c1))
391            });
392            if dup {
393                continue;
394            }
395            if contradiction_count >= contradictions_out.len() {
396                return Err(MaterializeError::ContradictionBufferFull);
397            }
398            contradictions_out[contradiction_count] = DisjointnessViolation {
399                individual: x,
400                class_a: c1,
401                class_b: c2,
402            };
403            contradiction_count += 1;
404        }
405    }
406
407    Ok(MaterializeSummary {
408        triple_count: len,
409        inferred_count: len - initial_len.min(triples.len()),
410        contradiction_count,
411        iterations,
412        saturated,
413    })
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419
420    // Stable test IRIs.
421    fn h(s: &str) -> u64 {
422        crate::q_hash(s)
423    }
424
425    #[test]
426    fn subclass_transitivity_and_type_propagation() {
427        let (alice, student, person, agent) = (h("alice"), h("Student"), h("Person"), h("Agent"));
428        let mut triples = [RdfTriple::new(0, 0, 0); 64];
429        triples[0] = RdfTriple::new(alice, RDF_TYPE, student);
430        triples[1] = RdfTriple::new(student, RDFS_SUBCLASS_OF, person);
431        triples[2] = RdfTriple::new(person, RDFS_SUBCLASS_OF, agent);
432        let mut contra = [DisjointnessViolation {
433            individual: 0,
434            class_a: 0,
435            class_b: 0,
436        }; 4];
437
438        let s = materialize_owl_rl(&mut triples, 3, &[], 16, &mut contra).unwrap();
439        assert!(s.saturated);
440        let out = &triples[..s.triple_count];
441        // scm-sco: Student ⊑ Agent
442        assert!(out.contains(&RdfTriple::new(student, RDFS_SUBCLASS_OF, agent)));
443        // cax-sco: alice is a Person AND an Agent
444        assert!(out.contains(&RdfTriple::new(alice, RDF_TYPE, person)));
445        assert!(out.contains(&RdfTriple::new(alice, RDF_TYPE, agent)));
446        assert_eq!(s.contradiction_count, 0);
447    }
448
449    #[test]
450    fn domain_and_range_typing() {
451        let (knows, person, x, y) = (h("knows"), h("Person"), h("x"), h("y"));
452        let mut triples = [RdfTriple::new(0, 0, 0); 32];
453        triples[0] = RdfTriple::new(knows, RDFS_DOMAIN, person);
454        triples[1] = RdfTriple::new(knows, RDFS_RANGE, person);
455        triples[2] = RdfTriple::new(x, knows, y);
456        let mut contra = [DisjointnessViolation {
457            individual: 0,
458            class_a: 0,
459            class_b: 0,
460        }; 2];
461
462        let s = materialize_owl_rl(&mut triples, 3, &[], 16, &mut contra).unwrap();
463        let out = &triples[..s.triple_count];
464        assert!(out.contains(&RdfTriple::new(x, RDF_TYPE, person))); // prp-dom
465        assert!(out.contains(&RdfTriple::new(y, RDF_TYPE, person))); // prp-rng
466    }
467
468    #[test]
469    fn transitive_and_inverse_properties() {
470        let (anc, has_child, a, b, c) = (h("ancestorOf"), h("hasChild"), h("a"), h("b"), h("c"));
471        let mut triples = [RdfTriple::new(0, 0, 0); 32];
472        triples[0] = RdfTriple::new(anc, RDF_TYPE, OWL_TRANSITIVE_PROPERTY);
473        triples[1] = RdfTriple::new(a, anc, b);
474        triples[2] = RdfTriple::new(b, anc, c);
475        triples[3] = RdfTriple::new(anc, OWL_INVERSE_OF, has_child);
476        let mut contra = [DisjointnessViolation {
477            individual: 0,
478            class_a: 0,
479            class_b: 0,
480        }; 2];
481
482        let s = materialize_owl_rl(&mut triples, 4, &[], 16, &mut contra).unwrap();
483        let out = &triples[..s.triple_count];
484        assert!(out.contains(&RdfTriple::new(a, anc, c))); // prp-trp
485        assert!(out.contains(&RdfTriple::new(b, has_child, a))); // prp-inv
486        assert!(out.contains(&RdfTriple::new(c, has_child, b)));
487    }
488
489    #[test]
490    fn property_chain_unrolling() {
491        // uncleOf ⊑ parentOf ∘ brotherOf
492        let (parent, brother, uncle, x, p, u) = (
493            h("parentOf"),
494            h("brotherOf"),
495            h("uncleOf"),
496            h("x"),
497            h("p"),
498            h("u"),
499        );
500        let mut triples = [RdfTriple::new(0, 0, 0); 16];
501        triples[0] = RdfTriple::new(x, parent, p);
502        triples[1] = RdfTriple::new(p, brother, u);
503        let chains = [ChainAxiom {
504            composed: uncle,
505            first: parent,
506            second: brother,
507        }];
508        let mut contra = [DisjointnessViolation {
509            individual: 0,
510            class_a: 0,
511            class_b: 0,
512        }; 2];
513
514        let s = materialize_owl_rl(&mut triples, 2, &chains, 16, &mut contra).unwrap();
515        assert!(triples[..s.triple_count].contains(&RdfTriple::new(x, uncle, u)));
516    }
517
518    #[test]
519    fn disjointness_isolation_does_not_halt() {
520        // Cat disjointWith Dog; fido inferred to be both (via subclass) — quarantine,
521        // but unrelated inference (rex is a Pet) must still complete.
522        let (cat, dog, animal, fido, rex, pet, breed) = (
523            h("Cat"),
524            h("Dog"),
525            h("Animal"),
526            h("fido"),
527            h("rex"),
528            h("Pet"),
529            h("Breed"),
530        );
531        let mut triples = [RdfTriple::new(0, 0, 0); 64];
532        triples[0] = RdfTriple::new(cat, OWL_DISJOINT_WITH, dog);
533        triples[1] = RdfTriple::new(fido, RDF_TYPE, cat);
534        triples[2] = RdfTriple::new(fido, RDF_TYPE, dog);
535        triples[3] = RdfTriple::new(breed, RDFS_SUBCLASS_OF, pet);
536        triples[4] = RdfTriple::new(rex, RDF_TYPE, breed);
537        let _ = animal;
538        let mut contra = [DisjointnessViolation {
539            individual: 0,
540            class_a: 0,
541            class_b: 0,
542        }; 4];
543
544        let s = materialize_owl_rl(&mut triples, 5, &[], 16, &mut contra).unwrap();
545        // The contradiction was isolated, not fatal.
546        assert_eq!(s.contradiction_count, 1);
547        assert_eq!(contra[0].individual, fido);
548        // Closure still completed: rex is a Pet (cax-sco) despite the inconsistency.
549        assert!(triples[..s.triple_count].contains(&RdfTriple::new(rex, RDF_TYPE, pet)));
550    }
551
552    #[test]
553    fn functional_property_implies_sameas() {
554        // hasSSN functional: x hasSSN a, x hasSSN b ⟹ a sameAs b (+ eq-sym).
555        let (has_ssn, x, a, b) = (h("hasSSN"), h("x"), h("idA"), h("idB"));
556        let mut triples = [RdfTriple::new(0, 0, 0); 16];
557        triples[0] = RdfTriple::new(has_ssn, RDF_TYPE, OWL_FUNCTIONAL_PROPERTY);
558        triples[1] = RdfTriple::new(x, has_ssn, a);
559        triples[2] = RdfTriple::new(x, has_ssn, b);
560        let mut contra = [DisjointnessViolation {
561            individual: 0,
562            class_a: 0,
563            class_b: 0,
564        }; 2];
565
566        let s = materialize_owl_rl(&mut triples, 3, &[], 16, &mut contra).unwrap();
567        let out = &triples[..s.triple_count];
568        assert!(
569            out.contains(&RdfTriple::new(a, OWL_SAME_AS, b))
570                || out.contains(&RdfTriple::new(b, OWL_SAME_AS, a))
571        );
572        // eq-sym closed it both ways.
573        assert!(out.contains(&RdfTriple::new(a, OWL_SAME_AS, b)));
574        assert!(out.contains(&RdfTriple::new(b, OWL_SAME_AS, a)));
575    }
576
577    #[test]
578    fn working_set_full_is_reported() {
579        let (a, sub) = (h("a"), RDFS_SUBCLASS_OF);
580        // A long subclass chain in a deliberately tiny buffer overflows on derivation.
581        let mut triples = [RdfTriple::new(0, 0, 0); 6];
582        triples[0] = RdfTriple::new(a, RDF_TYPE, h("C0"));
583        triples[1] = RdfTriple::new(h("C0"), sub, h("C1"));
584        triples[2] = RdfTriple::new(h("C1"), sub, h("C2"));
585        triples[3] = RdfTriple::new(h("C2"), sub, h("C3"));
586        let mut contra = [DisjointnessViolation {
587            individual: 0,
588            class_a: 0,
589            class_b: 0,
590        }; 2];
591        let r = materialize_owl_rl(&mut triples, 4, &[], 16, &mut contra);
592        assert_eq!(r, Err(MaterializeError::WorkingSetFull));
593    }
594}