growl 0.5.0

Rust bindings for the Growl OWL 2 RL reasoner
Documentation
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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# Rust Bindings for Growl

Safe Rust wrapper over the [Growl](../) OWL 2 RL reasoner. Provides RAII arena management, typed RDF terms, and a high-level API for ontology reasoning.

## Prerequisites

A C compiler is required. The `cc` crate handles compilation automatically; no SLOP toolchain is needed since pre-transpiled C sources are included in `csrc/`.

## Installation

```toml
[dependencies]
growl = "0.1"
```

## Quick Start

```rust
use growl::{Arena, IndexedGraph, ReasonerResult, Term, reason};

let arena = Arena::new(4 * 1024 * 1024);
let mut graph = IndexedGraph::new(&arena);

// Create terms
let dog = arena.make_iri("http://example.org/Dog");
let animal = arena.make_iri("http://example.org/Animal");
let fido = arena.make_iri("http://example.org/fido");
let rdf_type = arena.make_iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
let rdfs_subclass = arena.make_iri("http://www.w3.org/2000/01/rdf-schema#subClassOf");
let owl_class = arena.make_iri("http://www.w3.org/2002/07/owl#Class");

// Build graph
graph.add_triple(arena.make_triple(dog, rdfs_subclass, animal));
graph.add_triple(arena.make_triple(fido, rdf_type, dog));
graph.add_triple(arena.make_triple(dog, rdf_type, owl_class));
graph.add_triple(arena.make_triple(animal, rdf_type, owl_class));

// Run the reasoner
let result = reason(&arena, &graph);
match result {
    ReasonerResult::Success { graph: result_graph, inferred_count, .. } => {
        println!("Inferred {} triples", inferred_count);

        // Check what fido was inferred to be
        let types = growl::get_types(&arena, &result_graph, fido);
        for t in &types {
            println!("fido rdf:type {}", t);
        }
    }
    ReasonerResult::Inconsistent { reports } => {
        for r in &reports {
            println!("Inconsistent: {}", r.reason);
            for w in &r.witnesses {
                println!("  witness: {}", w);
            }
        }
    }
}
```

## High-Level API

The `Reasoner` struct owns its arena and graph, hiding FFI details. It returns `OwnedReasonerResult` with owned data that can be freely sent across threads.

```rust
use growl::{Reasoner, OwnedTerm, OwnedReasonerResult, ReasonerConfig};

let mut reasoner = Reasoner::new();  // 32 MB arena
// or: Reasoner::with_capacity(64 * 1024 * 1024)

// Add triples — all-IRI convenience method
reasoner.add_iri_triple(
    "http://example.org/Dog",
    "http://www.w3.org/2000/01/rdf-schema#subClassOf",
    "http://example.org/Animal",
);
reasoner.add_iri_triple(
    "http://example.org/fido",
    "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
    "http://example.org/Dog",
);

// Or add triples with OwnedTerm for literals and blank nodes
let s = OwnedTerm::Iri("http://example.org/fido".into());
let p = OwnedTerm::Iri("http://example.org/name".into());
let o = OwnedTerm::Literal {
    value: "Fido".into(),
    datatype: None,
    lang: Some("en".into()),
};
reasoner.add_triple(&s, &p, &o);

// Run reasoning
match reasoner.reason() {
    OwnedReasonerResult::Success { triples, inferred_count, iterations } => {
        println!("Inferred {} triples in {} iterations", inferred_count, iterations);
        for t in &triples {
            println!("  {}", t);
        }
    }
    OwnedReasonerResult::Inconsistent { reports } => {
        for r in &reports {
            println!("Inconsistent: {}", r.reason);
            for w in &r.witnesses {
                println!("  witness: {}", w);
            }
        }
    }
    OwnedReasonerResult::Cancelled { inferred_count, iterations, .. } => {
        println!("Cancelled after {} iterations ({} inferred)", iterations, inferred_count);
    }
}

// Enable complete mode (cls-thing, prp-ap, dt-type2)
let mut reasoner = Reasoner::new().complete(true);

// Enable enrich mode (property/subclass propagation only)
let mut reasoner = Reasoner::new().enrich(true);

// Or use custom config
let config = ReasonerConfig::new().verbose(false).fast(true);
let result = reasoner.reason_with_config(&config);

// Quick consistency check (no triples returned)
assert!(reasoner.is_consistent());
```

### Owned Types

`OwnedTerm`, `OwnedTriple`, and `OwnedReasonerResult` use `String` instead of borrowed `&str`, so they have no lifetime dependency on the arena and are `Send + Sync`.

```rust
enum OwnedTerm {
    Iri(String),
    Blank(i64),
    Literal { value: String, datatype: Option<String>, lang: Option<String> },
}

struct OwnedTriple {
    pub subject: OwnedTerm,
    pub predicate: OwnedTerm,
    pub object: OwnedTerm,
}

struct OwnedInconsistencyReport {
    pub reason: String,
    pub witnesses: Vec<OwnedTriple>,
}

enum OwnedReasonerResult {
    Success { triples: Vec<OwnedTriple>, inferred_count: i64, iterations: i64 },
    Inconsistent { reports: Vec<OwnedInconsistencyReport> },
    Cancelled { triples: Vec<OwnedTriple>, inferred_count: i64, iterations: i64 },
}
```

## Low-Level API

### `Arena`

RAII wrapper around the C arena allocator. All terms and graphs are allocated from an arena.

```rust
let arena = Arena::new(4 * 1024 * 1024);    // 4 MB
let arena = Arena::with_default_capacity();  // 1 MB

// Create RDF terms
let iri = arena.make_iri("http://example.org/x");
let blank = arena.make_blank(1);
let lit = arena.make_literal("hello", None, Some("en"));
let triple = arena.make_triple(iri, iri, lit);
```

### `IndexedGraph`

An indexed RDF graph with SPO/PSO/OSP lookups.

```rust
let mut graph = IndexedGraph::new(&arena);

// Add triples (raw FFI terms)
graph.add_triple(arena.make_triple(s, p, o));

// Add triples (safe Term values)
graph.add(&Term::Iri("http://example.org/s"),
          &Term::Iri("http://example.org/p"),
          &Term::Iri("http://example.org/o"));

graph.size();                          // number of triples
graph.contains_triple(triple);         // exact lookup

// Pattern matching — None means wildcard
graph.match_pattern(None, Some(p), Some(o));  // all triples with given p and o

// Convenience accessors
graph.objects(subject, predicate);     // Vec<Term>
graph.subjects(predicate, object);     // Vec<Term>
```

### `Term`

Safe, read-side view of an RDF term. Lifetime tied to the arena's string data.

```rust
enum Term<'a> {
    Iri(&'a str),
    Blank(i64),
    Literal { value: &'a str, datatype: Option<&'a str>, lang: Option<&'a str> },
}

// Convert from FFI
let term = Term::from_ffi(raw_term);

// Display formats: <http://...>, _:b42, "hello"@en
println!("{}", term);
```

### `Triple`

Safe, read-side view of an RDF triple.

```rust
struct Triple<'a> {
    pub subject: Term<'a>,
    pub predicate: Term<'a>,
    pub object: Term<'a>,
}

let triple = Triple::from_ffi(raw_triple);
println!("{}", triple);  // <s> <p> <o> .
```

### `ReasonerConfig`

Builder pattern configuration for the reasoner.

```rust
let config = ReasonerConfig::new()
    .worker_count(4)      // parallel workers (default: 4)
    .channel_buffer(256)  // message buffer size (default: 256)
    .max_iterations(1000) // iteration limit (default: 1000)
    .verbose(false)       // per-iteration timing (default: true)
    .fast(true)           // skip schema rules (default: false)
    .complete(false)      // enable cls-thing & prp-ap (default: false)
    .enrich(true)         // property/subclass enrichment only (default: false)
    .validate(true)       // enable validation mode (default: false)
    .validate_ns("http://example.org/")  // validate only this namespace
    .cancel_token(&token); // cooperative cancellation (see below)
```

### `ReasonerResult`

```rust
struct InconsistencyReport<'a> {
    pub reason: String,
    pub witnesses: Vec<Triple<'a>>,
}

enum ReasonerResult<'a> {
    Success { graph: IndexedGraph<'a>, inferred_count: i64, iterations: i64 },
    Inconsistent { reports: Vec<InconsistencyReport<'a>> },
    Cancelled { graph: IndexedGraph<'a>, inferred_count: i64, iterations: i64 },
}
```

### Annotation Filtering

Large ontologies often contain many annotation triples (`rdfs:label`, `rdfs:comment`, SKOS labels, Dublin Core metadata) that have no semantic effect under OWL 2 RL. Filtering them before reasoning can significantly reduce memory use and improve performance.

**High-level (Reasoner):**

```rust
let mut reasoner = Reasoner::new().filter_annotations(true);
reasoner.add_iri_triple("http://example.org/Dog", "http://www.w3.org/2000/01/rdf-schema#label", "Dog");
reasoner.add_iri_triple("http://example.org/Dog", "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", "http://www.w3.org/2002/07/owl#Class");

// Annotation triples are filtered before reasoning and restored in the result
let result = reasoner.reason();
```

**Low-level:**

```rust
let filtered = growl::filter_annotations(&arena, &graph);
let result = growl::reason(&arena, &filtered);
```

The list of 27 standard annotation properties is available as `growl::STANDARD_ANNOTATION_PROPERTIES`. User-declared `owl:AnnotationProperty` instances in the graph are also filtered.

### Validation

Validation mode checks TBox consistency by injecting a synthetic instance of each class and property, then running the reasoner to detect contradictions (e.g., unsatisfiable classes due to disjointness or restriction conflicts). It reports **all** unsatisfiable entities, not just the first.

A namespace prefix can scope validation to only entities within that namespace, which is useful when importing external ontologies that may have known issues.

**High-level (Reasoner):**

```rust
let mut reasoner = Reasoner::new().complete(true);
// ... add triples ...

// Validate all entities
let result = reasoner.validate();

// Validate only entities in a namespace
let result = reasoner.validate_ns("http://example.org/");

match result {
    OwnedValidateResult::Satisfiable => println!("All classes satisfiable"),
    OwnedValidateResult::Unsatisfiable { reports } => {
        for r in &reports {
            println!("{}: {}", r.entity, r.reason);
            for w in &r.witnesses {
                println!("  {}", w);
            }
        }
    }
}
```

**Low-level:**

```rust
let result = growl::validate(&arena, &graph);
let result = growl::validate_with_ns(&arena, &graph, "http://example.org/");
```

**Types:**

```rust
struct ValidateReport<'a> {
    pub entity: Term<'a>,
    pub reason: String,
    pub witnesses: Vec<Triple<'a>>,
}

enum ValidateResult<'a> {
    Satisfiable,
    Unsatisfiable { reports: Vec<ValidateReport<'a>> },
}

// Owned equivalents (Send + Sync)
struct OwnedValidateReport {
    pub entity: OwnedTerm,
    pub reason: String,
    pub witnesses: Vec<OwnedTriple>,
}

enum OwnedValidateResult {
    Satisfiable,
    Unsatisfiable { reports: Vec<OwnedValidateReport> },
}
```

### Cancellation

Reasoning can be cancelled cooperatively using a `CancelToken`. The token is checked at the start of each fixpoint iteration; when cancelled, the reasoner returns `Cancelled` with the partial graph computed so far. All triples in the partial graph are sound (correctly inferred), but the closure may be incomplete.

```rust
use growl::{Reasoner, ReasonerConfig, CancelToken, OwnedReasonerResult};
use std::thread;
use std::time::Duration;

let token = CancelToken::new();
let token2 = token.clone();  // CancelToken is Clone + Send + Sync

// Cancel from another thread after a timeout
thread::spawn(move || {
    thread::sleep(Duration::from_secs(5));
    token2.cancel();
});

let mut reasoner = Reasoner::new();
// ... add triples ...

let config = ReasonerConfig::new()
    .verbose(false)
    .cancel_token(&token);
let result = reasoner.reason_with_config(&config);

match result {
    OwnedReasonerResult::Cancelled { inferred_count, iterations, .. } => {
        println!("Cancelled after {} iterations ({} inferred)", iterations, inferred_count);
    }
    OwnedReasonerResult::Success { .. } => {
        println!("Completed before cancellation");
    }
    OwnedReasonerResult::Inconsistent { .. } => { /* ... */ }
}

// Tokens can be reset and reused
token.reset();
```

### Free Functions

```rust
// Run with default config
reason(&arena, &graph) -> ReasonerResult

// Run with custom config
reason_with_config(&arena, &graph, &config) -> ReasonerResult

// Quick consistency check (no graph returned)
is_consistent(&arena, &graph) -> bool

// Filter annotation triples
filter_annotations(&arena, &graph) -> IndexedGraph

// Validate TBox (check for unsatisfiable classes/properties)
validate(&arena, &graph) -> ValidateResult
validate_with_ns(&arena, &graph, "http://example.org/") -> ValidateResult

// Query helpers
get_types(&arena, &graph, individual) -> Vec<Term>
get_same_as(&arena, &graph, individual) -> Vec<Term>
```

## Thread Safety

The C runtime uses a global intern pool (`slop_global_intern_pool`) that is protected by a `pthread_mutex_t` (enabled via the `SLOP_INTERN_THREADSAFE` compile flag in `build.rs`). This means multiple arenas can safely be used concurrently from different threads.

The `Arena` type is still `!Send + !Sync`, individual arenas must not be shared across threads. Each thread should create its own arena.

## Running Tests

```bash
cd rust && cargo test
```

Tests can run with the default thread count since the intern pool is internally synchronized.

The build script (`build.rs`) compiles all C sources automatically via the `cc` crate.

## License

Apache License 2.0 — see [LICENSE](LICENSE).