Quantified Properties¶
Stainless supports explicit quantifiers through the
stainless.lang.Quantifiers library. Thanks to the instantiation of
universal quantifiers by the SMT solver, it is possible to use quantifiers
in a rather systematic and controlled way. This chapter explains how to
introduce and eliminate quantifiers following the rules of natural
deduction, and illustrates each rule with a Stainless example.
Importing the Quantifier Library¶
All examples below assume the following imports:
import stainless.lang._
import stainless.lang.Quantifiers.*
import stainless.annotation.*
import stainless.lang.StaticChecks.*
import stainless.lang.*
The library provides the following key constructs:
Forall(p): an opaque wrapper around the built-inforall, used to state that predicatepholds for all values of the quantified type.ForallOf(p)(a): instantiatesForall(p)at a concrete valuea, yieldingp(a).Exists(p): defined as!Forall(x => !p(x)), states that some value satisfiesp.ExistsThe(w)(p): introduces an existential by providing a witnesswwithp(w).pickWitness(p): givenExists(p), returns a witness satisfyingp.
There are also helpers for reasoning about negated quantifiers:
notExists(p): from!Exists(p)derivesForall(x => !p(x)).notForall(p): from!Forall(p)derivesExists(x => !p(x)).notExistsNot(p): from!Exists(x => !p(x))derivesForall(p).
Propositional Natural Deduction¶
Before discussing quantifiers, we review how Stainless handles the
propositional connectives &&, ||, !, and ==>.
Conjunction (&&)¶
Introduction. If p and q are both true, then p && q is true.
@ghost
def conjIntro(p: Boolean, q: Boolean): Unit = {
require(p)
require(q)
()
}.ensuring(_ => p && q)
Elimination. From p && q we can derive p (or q).
@ghost
def conjElimLeft(p: Boolean, q: Boolean): Unit = {
require(p && q)
()
}.ensuring(_ => p)
@ghost
def conjElimRight(p: Boolean, q: Boolean): Unit = {
require(p && q)
()
}.ensuring(_ => q)
Disjunction (||)¶
Introduction. From p alone, we can derive p || q.
@ghost
def disjIntroLeft(p: Boolean, q: Boolean): Unit = {
require(p)
()
}.ensuring(_ => p || q)
Elimination. If p || q holds and both p ==> r and q ==> r
hold, then r holds.
@ghost
def disjElim(p: Boolean, q: Boolean, r: Boolean): Unit = {
require(p || q)
require(p ==> r)
require(q ==> r)
()
}.ensuring(_ => r)
Negation (!)¶
Double Negation Elimination. From !!p, derive p.
@ghost
def doubleNegElim(p: Boolean): Unit = {
require(!(!p))
()
}.ensuring(_ => p)
Double Negation Introduction. From p, derive !!p.
@ghost
def doubleNegIntro(p: Boolean): Unit = {
require(p)
()
}.ensuring(_ => !(!p))
Implication (==>)¶
Modus Ponens. From p ==> q and p, derive q.
@ghost
def modusPonens(p: Boolean, q: Boolean): Unit = {
require(p ==> q)
require(p)
()
}.ensuring(_ => q)
Modus Tollens. From p ==> q and !q, derive !p.
@ghost
def modusTollens(p: Boolean, q: Boolean): Unit = {
require(p ==> q)
require(!q)
()
}.ensuring(_ => !p)
Quantifier Rules¶
Universal Quantification (Forall)¶
Introduction (∀I). To prove Forall(p), one must show that p(x)
holds for an arbitrary x. In Stainless this is done by writing a
function whose body proves p(x) for a universally quantified variable.
The built-in forall is Skolemized when it needs to be proven.
@ghost
def forallIntro: Unit = {
assert(stainless.lang.forall((x: BigInt) => x + 0 == x))
}
Elimination (∀E). Given Forall(p), derive p(a) for any concrete
a by calling ForallOf(p)(a).
@ghost
def forallElim(p: BigInt => Boolean, a: BigInt): Unit = {
require(Forall(p))
ForallOf(p)(a)
}.ensuring(_ => p(a))
Existential Quantification (Exists)¶
Introduction (∃I). To prove Exists(p), provide a witness w
such that p(w) holds, then call ExistsThe(w)(p).
@ghost
def existsIntro: Unit = {
val p = (x: BigInt) => x > 0 && x < 10
val witness: BigInt = 5
assert(p(witness))
ExistsThe(witness)(p)
}.ensuring(_ => Exists((x: BigInt) => x > 0 && x < 10))
Elimination (∃E). Given Exists(p), obtain a witness w
satisfying p(w) by calling pickWitness(p).
@ghost
def existsElim(p: BigInt => Boolean): BigInt = {
require(Exists(p))
val w: BigInt = pickWitness[BigInt](p)
assert(p(w))
w
}.ensuring(res => p(res))
Negated Quantifiers¶
The following rules connect negation and quantification, mirroring the classical equivalences.
¬∃ → ∀¬: From !Exists(p), derive Forall(x => !p(x)).
@ghost
def notExistsToForallNot(p: BigInt => Boolean): Unit = {
require(!Exists(p))
notExists(p)
}.ensuring(_ => Forall((x: BigInt) => !p(x)))
¬∀ → ∃¬: From !Forall(p), derive Exists(x => !p(x)).
@ghost
def notForallToExistsNot(p: BigInt => Boolean): Unit = {
require(!Forall(p))
notForall(p)
}.ensuring(_ => Exists((x: BigInt) => !p(x)))
¬∃¬ → ∀: From !Exists(x => !p(x)), derive Forall(p).
@ghost
def notExistsNotToForall(p: BigInt => Boolean): Unit = {
require(!Exists((x: BigInt) => !p(x)))
notExistsNot(p)
}.ensuring(_ => Forall(p))
Combined Examples¶
The following examples show how to combine multiple rules in one proof.
Conjunction of universals. From Forall(p) and Forall(q),
derive that p(x) && q(x) holds for all x.
@ghost
def forallConjunction(p: BigInt => Boolean, q: BigInt => Boolean): Unit = {
require(Forall(p))
require(Forall(q))
unfold(Forall(p))
unfold(Forall(q))
assert(stainless.lang.forall((x: BigInt) => p(x) && q(x)))
}
Weakening an existential. From Exists(p),
derive Exists(x => p(x) || q(x)).
@ghost
def existsWeaken(p: BigInt => Boolean, q: BigInt => Boolean): Unit = {
require(Exists(p))
val w = pickWitness[BigInt](p)
assert(p(w) || q(w))
ExistsThe(w)((x: BigInt) => p(x) || q(x))
}.ensuring(_ => Exists((x: BigInt) => p(x) || q(x)))
Universal modus ponens. From Forall(x => p(x) ==> q(x)) and
Forall(p), derive that q(x) holds for all x.
@ghost
def forallModusPonens(p: BigInt => Boolean, q: BigInt => Boolean): Unit = {
require(Forall((x: BigInt) => p(x) ==> q(x)))
require(Forall(p))
unfold(Forall((x: BigInt) => p(x) ==> q(x)))
unfold(Forall(p))
assert(stainless.lang.forall((x: BigInt) => q(x)))
}
Summary¶
The following table summarizes the rules and the corresponding Stainless constructs:
Rule |
Logical Statement |
Stainless Construct |
|---|---|---|
∧-Introduction |
P, Q ⊢ P ∧ Q |
|
∧-Elimination |
P ∧ Q ⊢ P |
Extract from |
∨-Introduction |
P ⊢ P ∨ Q |
|
∨-Elimination |
P ∨ Q, P→R, Q→R ⊢ R |
Case analysis + implications |
¬¬-Elimination |
¬¬P ⊢ P |
Automatic by SMT solver |
→-Elimination |
P→Q, P ⊢ Q |
|
∀-Introduction |
⊢ ∀x.P(x) |
|
∀-Elimination |
∀x.P(x) ⊢ P(a) |
|
∃-Introduction |
P(w) ⊢ ∃x.P(x) |
|
∃-Elimination |
∃x.P(x) ⊢ P(w) for some w |
|
¬∃ → ∀¬ |
¬∃x.P(x) ⊢ ∀x.¬P(x) |
|
¬∀ → ∃¬ |
¬∀x.P(x) ⊢ ∃x.¬P(x) |
|
¬∃¬ → ∀ |
¬∃x.¬P(x) ⊢ ∀x.P(x) |
|
The complete set of test cases can be found in frontends/benchmarks/verification/valid/NaturalDeduction.scala.