# 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
.max_triples(0) // cancel if graph exceeds N triples (default: 0 = no limit)
.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
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).