Jean-Baptiste Tristan & K. Rustan M. Leino |
assume
assert
:|
:=
if
forall
Recall that a lemma can be understood as a set of propositions
When a proof is not provided, these propositions are axioms
When a proof is provided, the lemma can be understood as a proof challenge
Example:
lemma Example(x: int)
requires x % 2 == 0
ensures 2 * (x / 2) == x
Assuming (hypothesis):
x
of type int
x % 2 == 0
holds
Prove (goal):
2 * (x / 2) == x
x: int, x % 2 == 0 |- 2 * (x / 2) == x
|-
are hypothesis
|-
is the goal
|-
as “prove”
At a high level, Dafny verifies a lemma by challenging an automated theorem prover (ATP)
Recall that a proof script is a sequence of statements and consider the simplest possible proof: the empty sequence
lemma Example(x: int)
requires x % 2 == 0
ensures 2 * (x / 2) == x
{
}
In this case, the proof challenge is simply derived from the lemma itself:
x: int, x % 2 == 0 |- 2 * (x / 2) == x
It so happens that, in this example, the ATP will meet the challenge
One challenge that any ATP should meet is to prove that something we are assuming holds
lemma AndIntro<T1, T2, T3, T4>(x1: T1, x2: int, x3: int -> T4, P: bool, Q: T1 -> bool)
requires Q(x1)
requires P
ensures P
{}
Several important notes:
P
, Q
, and R
could stand for any propositions/predicates
P
is assumed
The essence of this challenge can be summarized as:
P
is in the list of propositions you assume to hold, then it holds
We use a frugal notation to capture the essence of the challenge
Γ, P ⊢ P
Where:
P
to the end for convenience of notation
Proving a conjunction
Γ, P, Q ⊢ P && Q
Using a conjunction
Γ, P && Q ⊢ P
Γ, P && Q ⊢ Q
Proving a disjunction
Γ, P ⊢ P || Q
Γ, Q ⊢ P || Q
Using a disjunction
Γ, P || Q, P ==> R, Q ==> R ⊢ R
Proving a negation
Γ, P ==> false ⊢ !P
Using a negation
Γ, P, !P ⊢ false
If you assume something that is false
, anything can be concluded
This is particularly useful to construct proofs incrementally
Γ, false ⊢ P
Proving an equivalence
Γ, P ==> Q, Q ==> P ⊢ P <==> Q
Using a equivalence
Γ, P <==> Q ⊢ P ==> Q
Γ, P <==> Q ⊢ Q ==> P
Proving an existential quantifier
Γ, T, k: T, P(k) ⊢ exists c: T :: P(c)
Using an existential quantifier
Γ, T, c: T, P(c), (exists x: T :: P(x)) ==> Q ⊢ Q
Γ, T, forall x: T :: P(x), c: T ⊢ P(c)
How does Dafny generate proof challenges from a lemma and a proof script?
This question could be answered precisely, but we will not!
Instead, we would like to capture the idea that writing a proof is a linear process
As a first approximation, you can think of a statement as something that modifies the proof challenge
Then, the goal of the proof script is:
lemma Example<T>(x: T)
requires P(x)
ensures Q(x)
{
// T, x: T, P(x) |- Q(x)
// ...
// T, x: T, P(x), Q(x) |- Q(x)
}
lemma Example<T>(x: T)
requires P(x)
ensures Q(x)
{
// T, x: T, P(x) |- Q(x)
assert R(x) by {
// T, x: T, P(x) |- R(x)
}
// T, x: T, P(x), R(x) |- Q(x)
}
lemma Conclusion<T>(x: T)
requires P(x)
ensures Q(x)
{
// T, x: T, P(x) |- Q(x)
assume R(x);
// T, x: T, P(x), R(x) |- Q(x)
}
lemma Premise<T>(x: T)
requires P(x)
ensures R(x)
lemma Conclusion<T>(x: T)
requires P(x)
ensures Q(x)
{
// T, x: T, P(x) |- Q(x)
Premise<T>(x); // T, x: T, P(x) |- P(x)
// T, x: T, P(x), R(x) |- Q(x)
}
lemma Premise<T>(x: T)
ensures P(x) ==> R(x)
lemma Conclusion<T>(x: T)
requires P(x)
ensures Q(x)
{
// T, x: T, P(x) |- Q(x)
Premise<T>(x);
// T, x: T, P(x), P(x) ==> R(x) |- Q(x)
}
lemma Premise<T>()
ensures forall x: T :: P(x) ==> R(x)
lemma Conclusion<T>(x: T)
requires P(x)
ensures Q(x)
{
// T, x: T, P(x) |- Q(x)
Premise<T>();
// T, x: T, P(x), forall x: T :: P(x) ==> R(x) |- Q(x)
}
lemma Conclusion<T>(x: T)
requires P(x)
ensures Q(x)
{
// T, x: T, P(x) |- Q(x)
if R(x) {
// T, x: T, P(x), R(x) |- S(x)
}
// T, x: T, P(x), R(x) ==> S(x) |- Q(x)
}
lemma Premise<T>(x: T)
requires P(x)
ensures R(x)
lemma Conclusion<T>(x: T)
ensures P(x) ==> Q(x)
{
// T , x: T |- P(x) ==> Q(x)
if P(x) {
Premise(x); // T, x: T, P(x) |- P(x)
// T, x: T, P(x) |- Q(x)
}
// T, x: T, P(x) ==> R(x) |- P(x) ==> Q(x)
}
lemma Conclusion<T>(x: T)
requires P(x)
ensures Q(x)
{
// T, x: T, P(x) |- Q(x)
forall x: T
ensures R(x)
{
// T , x: T, P(x) |- R(x)
}
// T, x: T, P(x), forall x: T :: R(x) |- Q(x)
}
lemma Conclusion<T>()
requires exists y: T :: P(y)
ensures A
{
// T, exists x: T :: P(x) |- A
var y: T :| P(y);
// T, y: T, P(y) |- A
}
lemma Conclusion<T>(x: T)
ensures A
{
// T, x: T |- A
var y: T := x;
// T, x: T, y: T, y == x |- A
}
lemma Premise<T>(x: T)
requires P(x)
ensures Q(x)
lemma Conclusion<T>()
ensures forall x: T :: P(x) ==> Q(x)
{
// T |- forall x: T :: P(x) ==> Q(x)
forall x: T
ensures P(x) ==> Q(x)
{
// T , x: T |- P(x) ==> Q(x)
if P(x) {
Premise(x); // T, x: T, P(x) |- P(x)
// T, x: T, P(x) |- Q(x)
}
// T, x: T, P(x) ==> Q(x) |- P(x) ==> Q(x)
}
// T , forall x: T :: P(x) ==> Q(x) |- forall x: T :: P(x) ==> Q(x)
}
A proof is not done until you have removed all uses of assume
statements
In the end, you do not want any assume
to appear in your proof
However, assume
is a fantastic tool to grow a proof and make sure that it is going in the right direction
One by one, you will need to replace an assume
, and this can be done in one of two ways
lemma Example()
{
// ...
assume A; // TODO
// ...
}
lemma Example()
{
// ...
assert A by {
assume false; // TODO
}
// ...
}
lemma Prior()
ensures A
lemma Example()
{
// ...
Prior();
// ...
}
lemma Example()
ensures A && B
{
// |- A && B
assume A;
// A |- A && B
assume B;
// A , B |- A && B
}
lemma Example()
ensures A && B
{
// |- A && B
assume A && B;
// A && B |- A && B
}
|
$\Rightarrow$ |
|
lemma Example()
ensures A || B
{
// |- A || B
assume A;
// A |- A || B
}
lemma Example()
ensures A || B
{
// |- A || B
assume B;
// B |- A || B
}
|
$\Rightarrow$ |
|
|
$\Rightarrow$ |
|
|
$\Rightarrow$ |
|
|
$\Rightarrow$ |
|
lemma Example()
ensures A ==> B
{
// |- A ==> B
if A {
assume B;
}
// A ==> B |- A ==> B
}
|
$\Rightarrow$ |
|
|
$\Rightarrow$ |
|
lemma Example()
ensures !A
{
// |- !A
if A {
assume false;
}
// A ==> false |- !A
}
|
$\Rightarrow$ |
|
lemma Exampl<T>()
ensures forall x: T :: P(x)
{
// |- forall x: T :: P(x)
forall x: T
ensures P(x)
{
assume P(x);
}
// forall x: T :: P(x) |- forall x: T :: P(x)
}
|
$\Rightarrow$ |
|
lemma Example<T>(c: T)
ensures exists x: T :: P(x)
{
// |- exists x: T :: P(x)
assume P(c);
// exists x: T :: P(x) |- exists x: T :: P(x)
}
|
$\Rightarrow$ |
|
lemma Example()
requires A ==> B
ensures B
{
// A ==> B |- B
assume A;
// A ==> B, A |- B
}
lemma Example()
requires A || B
ensures C
{
// A || B |- C
if A {
assume C;
}
// A || B, A ==> C |- C
if B {
assume C;
}
// A || B, A ==> C, B ==> C |- C
}
lemma Example()
requires !A
ensures false
{
// !A |- false
assume A;
// !A, A |- false
}
lemma Example<T>()
requires exists c: T :: P(c)
ensures C
{
// exists c: T :: P(c) |- C
assume forall x: T :: P(x) ==> C;
// exists c: T :: P(c), forall x: T :: P(x) ==> C |- C
var c: T :| P(c);
// exists c: T :: P(c), c:T, P(c), forall x: T :: P(x) ==> C |- C
}
|
$\Rightarrow$ |
|
|
$\Rightarrow$ |
|
We have seen the basics of formalizing proofs
Rules of deduction
How they can be encoded in Dafny
Let's formalize the proof of the following lemma
Lemma 1.
Lemma 2.
Proof.
Let $x$ be some fixed but arbitrary natural number.
We assume that
$x\%2 = 0$.
Since the remainder of the division of $x$ by $2$
is $0$, then by definition, $x$ is divisible by $2$ and has a
unique divisor.
Let's call this divisor $y$, we have that $x =2 \times y$.
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// |- GOAL
assume false; // TODO
// false |- GOAL
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// |- GOAL
assume forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y; // TODO
// GOAL |- GOAL
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// |- GOAL
forall x: nat
ensures x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// x: nat |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
assume false; // TODO
// x: nat, false |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
}
// GOAL |- GOAL
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// |- GOAL
forall x: nat
ensures x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// x: nat |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
assume x % 2 == 0 ==> exists y: nat :: x == 2 * y; // TODO
// x: nat, x % 2 == 0 ==> exists y: nat :: x == 2 * y
// |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
}
// GOAL |- GOAL
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// |- GOAL
forall x: nat
ensures x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// x: nat |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
if x % 2 == 0 {
// x: nat, x % 2 == 0 |- exists y: nat :: x == 2 * y
assume false; // TODO
// x: nat, x % 2 == 0, false |- exists y: nat :: x == 2 * y
}
// x: nat, x % 2 == 0 ==> exists y: nat :: x == 2 * y
// |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
}
// GOAL |- GOAL
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// |- GOAL
forall x: nat
ensures x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// x: nat |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
if x % 2 == 0 {
// x: nat, x % 2 == 0 |- exists y: nat :: x == 2 * y
assert exists y: nat :: x == 2 * y by {
// x: nat, x % 2 == 0 |- exists y: nat :: x == 2 * y
assume false; // TODO
// x: nat, x % 2 == 0, false |- exists y: nat :: x == 2 * y
}
// x: nat, x % 2 == 0, exists y: nat :: x == 2 * y |- exists y: nat :: x == 2 * y
}
// x: nat, x % 2 == 0 ==> exists y: nat :: x == 2 * y
// |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
}
// GOAL |- GOAL
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// |- GOAL
forall x: nat
ensures x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
// x: nat |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
if x % 2 == 0 {
// x: nat, x % 2 == 0 |- exists y: nat :: x == 2 * y
assert exists y: nat :: x == 2 * y by {
// x: nat, x % 2 == 0 |- exists y: nat :: x == 2 * y
assert x == 2 * (x / 2);
// x: nat, x % 2 == 0, exists y: nat :: x == 2 * y |- exists y: nat :: x == 2 * y
}
// x: nat, x % 2 == 0, exists y: nat :: x == 2 * y |- exists y: nat :: x == 2 * y
}
// x: nat, x % 2 == 0 ==> exists y: nat :: x == 2 * y
// |- x % 2 == 0 ==> exists y: nat :: x == 2 * y
}
// GOAL |- GOAL
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
assume false; // TODO
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
forall x: nat
ensures x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
assume false; // TODO
}
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
forall x: nat
ensures x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
if x % 2 == 0 {
assume false; // TODO
}
}
}
lemma s1()
ensures forall x: nat :: x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
forall x: nat
ensures x % 2 == 0 ==> exists y: nat :: x == 2 * y
{
if x % 2 == 0 {
assert x == 2 * (x / 2);
}
}
}
Instead of:
lemma OddFactor(n: nat)
requires n % 2 == 1
ensures exists m: nat :: n == m * 2 + 1
{
assert n == n/2 * 2 + 1;
}
In the same way that you would prove $2 + 2 = 4$ by doing the math:
lemma OffFactorComputed(n: nat) returns (m: nat)
requires n % 2 == 1
ensures n == m * 2 + 1
{
m := n/2;
}