LeanRDF

Rules, derivations and soundness

Entailment quantifies over all interpretations, which no program can enumerate. Reasoners instead apply rules: if these triples are present, add that one. LeanRDF writes rules as data and proves each one sound.

Rules as data

inductive Pat where
  | var (n : String)
  | const (t : Term)

structure TPat where
  s : Pat
  p : Pat
  o : Pat

structure Rule where
  name : String
  body : List TPat
  head : TPat
  guards : List Guard := []

Rule rdfs9, transcribed from RDF 1.1 Semantics §9.2.1 with the specification's variable names:

def rdfs9 : Rule :=
  { name := "rdfs9"
    body := [⟨v "xxx", c rdfs.subClassOf, v "yyy"⟩, ⟨v "zzz", c rdf.type, v "xxx"⟩]
    head := ⟨v "zzz", c rdf.type, v "yyy"⟩ }

Variables may stand in any position: rdfs7 has a variable predicate. Every rule is range-restricted (head variables occur in the body) and names are unique; both are decidable and checked by decide in the tests.

Some patterns need a side condition. A Guard is a decidable test with a meaning and a proof that the test implies the meaning:

inductive Guard where
  | containerMembership (v : String)   -- the value is rdf:_n, n ≥ 1
  | stringLiteral (v : String)         -- an xsd:string literal
  | langLiteral (v : String)           -- a language-tagged literal

theorem Guard.check_sound (h : g.check σ = true) : g.Holds σ

rdfsRules has 67 rules: rdfs2–rdfs13, rdfD2, GrdfD1 for both datatypes, three container-membership axioms, and the 48 finite axiomatic triples as rules without premises.

Derivations

inductive Derives (rules : List Rule) (g : Graph) : Triple → Prop where
  | fact {t : Triple} : t ∈ g → Derives rules g t
  | apply (r : Rule) (σ : Subst) : r ∈ rules → (∀ gd ∈ r.guards, gd.Holds σ) →
      (∀ b ∈ r.body, Derives rules g (b.apply σ)) → Derives rules g (r.head.apply σ)

A triple is derivable if it is in the graph, or the head of a rule instance whose guards hold and whose body instances are derivable.

Soundness, one rule at a time

A rule is sound when, in every interpretation with the core conditions and under every blank-node mapping, true premises make the conclusion true:

def Rule.Sound (r : Rule) : Prop :=
  ∀ (I : SimpleInterp), RDFSCore I → ∀ (A : BlankNode → I.Dom) (σ : Subst),
    (∀ gd ∈ r.guards, gd.Holds σ) → (∀ b ∈ r.body, I.Holds A (b.apply σ)) →
    I.Holds A (r.head.apply σ)

The proof for rdfs9 reads like the textbook argument: unpack the two premises, observe that both mention the same class, and apply the subclass condition.

theorem rdfs9_sound : rdfs9.Sound := by
  intro I hI A σ _ hb
  obtain ⟨x, y, hx, hy, h1⟩ := holds_iri (hb _ (.head _))
  obtain ⟨z, x', hz, hx', h2⟩ := holds_iri (hb _ (.tail _ (.head _)))
  cases same hx hx'
  exact Holds.intro I hz rfl hy (hI.subClass_ext _ _ h1 _ h2)

All 67 are collected in rdfsRules_sound. Induction on Derives then gives the key theorem:

theorem Derives.sound {g : Graph} {t : Triple} (h : Derives rdfsRules g t) :
    g.RDFSEntails (Graph.singleton t)

Queries with blank nodes

A query is a graph, and its blank nodes are existential. If replacing them by terms (an instance mapping μ) makes every query triple derivable, the query is entailed:

theorem Derives.sound_graph {g q : Graph} (μ : BlankNode → Term)
    (h : ∀ t ∈ q, Derives rdfsRules g (t.instantiate μ)) : g.RDFSEntails q

The proof builds a blank-node assignment from the denotations of μ's values; it needs the RDF fact that every property is a resource.

These theorems use only propext, Quot.sound and Classical.choice, and a test pins that with #print axioms.