How shapes are compiled¶
Shifty compiles a shapes graph into a smaller path and shape algebra, then normalizes and plans it before evaluating data. Several SHACL constraint components become the same algebra operator, so validation, evidence, and repair can share their core traversal.
The core algebra¶
The IR comes from the SHACL fragment of Common Foundations for SHACL, ShEx, and PG-Schema (Ahmetaj et al.), specialized to RDF. It has two parts.
Paths (π) — a Kleene algebra with converse, denoting a relation over terms:
π ::= id | q | π⁻ | π · π′ | π ∪ π′ | π*
Identity, a predicate, inverse, sequence, alternation, and reflexive-transitive
closure. SHACL’s zeroOrMorePath is π*; oneOrMorePath is π · π*
and zeroOrOnePath is π ∪ id, both normalized away in the parser rather
than carried as IR constructors.
Shapes (φ) — a boolean algebra over a small set of atoms:
φ ::= ⊤ | test(c) | test(τ) | closed(Q) | eq(π,p) | disj(π,p)
| ¬φ | φ ∧ φ′ | φ ∨ φ′ | ∃≥ⁿ π.φ | ∃≤ⁿ π.φ
test(c) is equality with a constant, test(τ) is membership in a value
type, and the two counting forms are lower and upper bounds on how many
π-successors satisfy φ. Shifty adds a few operators the paper’s core does not
have but real SHACL needs — node kinds, lessThan, lessThanOrEquals,
uniqueLang — and fuses the two counting forms into one Count node with
optional min and max, because real SHACL always emits them as a pair on
a shared path and qualifier.
A schema is a set of (selector, φ) statements. The selector is the
target: which nodes to check. The graph conforms when every node a selector
picks satisfies the corresponding φ.
One counting primitive¶
The single most useful consequence is that Count subsumes a large slice of
the SHACL vocabulary. sh:minCount and sh:maxCount are counts. Qualified
cardinality is a count with a non-trivial qualifier. sh:node and
sh:property nesting is a count along a path. And universal quantification
— “every value of this path satisfies φ”, which is what sh:datatype on a
property shape means — is:
∀π.φ ≜ ∃≤0 π.¬φ
“at most zero π-successors fail φ”. So the planner optimizes one construct rather than a dozen vocabulary terms, and evidence, repair, and the cost model each need one case for all of them.
This encoding is visible in validation results. A sh:datatype violation is
reported as a CountHigh against a max 0 that does not appear explicitly
in the shapes graph. See Explain a validation result for a worked
example and Python API reference for the structured result fields.
The pipeline¶
Compilation is paid once per shapes graph. Prepared validators reuse the compiled plan across data graphs. The W3C report traversal is described under Result interfaces.¶
shifty inspect --stage <stage> prints the output of each layer; see
Inspect how shapes were compiled for worked output.
Parse (``rdf``). The shapes graph as triples.
Lower (``algebra``). SHACL vocabulary becomes π and φ. Shapes go into an arena and refer to each other by index, so a shared sub-shape is one node with several parents, and cyclic references are representable rather than an infinite structure.
Normalize (``normalized``). Semantics-preserving rewrites. The enabler is
hash-consing: structurally identical nodes are interned to one, which makes
sharing explicit and equality a pointer comparison. On top of that sit the
boolean laws (flattening, ⊤/⊥ absorption, idempotence, complementation, and
negation-normal form), the counting laws (unsatisfiable bounds collapse to ⊥,
counts on the same path and qualifier merge, an id path collapses the count
away), the path laws (π·id = π, (π⁻)⁻ = π, converse pushed down to
wrap only predicates, (π ∪ id)* = π*), and value-type tightening — merging
overlapping ranges, and detecting contradictions like a numeric range conjoined
with xsd:string, which becomes ⊥ and then absorbs upward.
Each rewrite is checked against the unoptimized evaluator as an oracle: the normalized schema and the original must agree on conformance and on which foci violate.
Analyse recursion (``strata``). The shape dependency graph is built with polarity-aware edges and condensed into strongly connected components. A schema whose recursion passes through a negation is refused. Recursion and stratification covers this.
Plan (``plan``). Two decisions. Focus nodes are seeded from an index rather
than a scan — sh:targetClass ex:Person becomes a lookup along
rdf:type/rdfs:subClassOf*. And conjunctions are reordered by estimated
cost, so a cheap discriminating check runs before an expensive one and
short-circuits it.
Execute. The plan runs over an indexed dataset. SPARQL constraints are
classified by whether they can be executed natively against those indexes or
need a general SPARQL engine; inspect --stage capability reports which.
Infer. SHACL-AF rules are a separate machine — bodies are condition shapes plus a selector, heads are triples built from node expressions — evaluated to a fixed point before validation, over the same arena and the same stratification.
The compiled rules come from the shapes graph; inference reads the data and shapes graph when the inputs are separate. The resulting data snapshot is what validation checks.
One compiled source, many data snapshots¶
CompiledShapes retains the authored and normalized shapes, rule and function
metadata, parsed query templates, and a lazily encoded source index. Sessions
created from it share that source storage. Each EvaluationSession owns its
asserted data and a local index extension; inferred facts are committed to that
index at rule-group boundaries. Validation, reports, and evidence reuse the
resulting dataset, including after inference.
The dataset exposes data, shapes, and their union as graph views. Native SPARQL
and the Spareval fallback read those same views, while $shapesGraph names the
unchanged source graph. The source and data keep separate membership even when
they contain an equal triple, and blank nodes from separately parsed documents
retain distinct identities. A requested public data graph is projected lazily;
ordinary validation does not build a second full graph or Oxigraph Store.
Every triple remains available in a complete predicate-partitioned primary
index. Reverse predicate indexes and general subject/object directories are
admitted from compiled access demand or observed probes under byte budgets.
Declined indexes use correct scans. shifty inspect --stage access shows the
data-independent read demands; --profile shows runtime index decisions,
scan work, and cache activity. The measured lifecycle and memory effects are
recorded in benchmark/shared-dataset-results.md in the repository.
Why one IR matters¶
The pipeline is the visible payoff, but the structural one is that algebraic validation, evidence, and repair traverse the same shape arena. Inference uses its own compiled rule program over the shared dataset.
Algebraic validation computes a boolean over the shape arena. Evidence
materializes a derivation from the same constraints. Repair is a fold over that
proof tree in the opposite direction: to describe how to fix φ₁ ∧ φ₂ you
need the repair spaces of both conjuncts, which is exactly what a fold gives
you. The shape enum has around fifteen variants and is already in
negation-normal form, so each of these folds is a manageable match rather than a
sprawl.
Evidence uses the algebraic evaluator to decide conformance, and the repair gate checks a proposed edit with that evaluator. The W3C report path has a separate constraint traversal, described below.
Result interfaces¶
validate_algebra() returns structured violations and nested algebraic
reasons. The CLI’s default text and JSON summaries render this result model.
This path does not build a W3C report graph. Evidence adds passing evaluations and derivations;
shape maps extract typed property bindings.
validate() returns (conforms, report_graph, results_text). The report
graph uses the W3C sh:ValidationReport vocabulary and requires rdflib
in Python; shifty validate --report emits the graph as Turtle. This path
projects authored SHACL components through a separate traversal of the source
RDF. It does not expose passing nodes or their derivations.
Both paths use CompiledShapes for schema admission, graph roles, and the
function registry. Their remaining constraint evaluation branches are separate,
so an implementation change must be checked in both. Choose the W3C path for
interoperability with SHACL tools and the algebraic path for structured
application findings. Exact fields are in Python API reference.
The algebra also explains the shape of the limitations. Repair is undefined for
sh:sparql not because nobody has written that case yet, but because an
arbitrary SPARQL query is opaque to the algebra — there is nothing to fold
over. The features that are hard are exactly the ones that escape the IR.
What the compilation costs¶
Compiling is a fixed cost paid before any data is looked at, and for a large ontology it is substantial. A 16-triple Brick model still takes seconds to validate against a 229k-triple shapes closure, essentially all of it setup.
That is fine when the schema is reused and terrible when it is not. It is why
PreparedValidator exists, why the evidence and repair sessions are objects
you hold rather than functions you call, and why the benchmark chart in
Benchmarks separates setup from the rest — a release that halves
validation time is invisible in the total if setup dominates.
Further reading¶
The design documents in development_docs/ are the primary sources:
00-formalism.md fixes the IR, 03-recursion-semantics.md the recursion
decision, 04-normalization.md the rewrite checklist with soundness notes,
05-sparql-execution.md the native SPARQL subset, and 06-repair.md the
repair API.