Jean-Baptiste Tristan & K. Rustan M. Leino |
The lemmas we have declared so far are (schemas of) axioms
By declaring the lemma we are making a statement that it is true
lemma Axiom()
ensures forall m: int, n: int :: m + n == n + m
Rather than:
ghost const x: int
lemma EssenceOfX()
ensures x == 0
A constant can be defined by assigning it an expression
ghost const x: int := 0
Rather than:
ghost function Increment(n: int): int
lemma EssenceOfIncrement(n: int)
ensures Increment(n) == n + 1
A function can be defined by providing it with a body
This body can be a functional expression
ghost function Increment(n: int): int {
n + 1
}
The body of a mathematical function can also be a non-functional expression
ghost predicate Valid(P: int -> bool) {
forall x: int :: P(x)
}
In general, a mathematical function may or may not be computable
Rather than:
ghost function Divide(m: int, n: int): int
lemma EssenceOfDivide(m: int, n: int)
requires n != 0
ensures Divide(m, n) == m / n
You can define a partial function
ghost function Divide(m: int, n: int): int
requires n != 0
{
m / n
}
Hilbert's epsilon operator can be used to define functions
predicate P<T>(m: T, n: T)
ghost predicate Prop<T(!new)>(m: T) {
exists n: T :: P(m, n)
}
ghost function F<T>(m: T): T
requires Prop(m)
{
var n :| P(m, n);
n
}
You will rarely need this for routine program verification
A function can be recursive
ghost function Factorial(n: int): int
lemma FactorialProp0()
ensures Factorial(0) == 1
lemma FactorialProp1(n: int)
requires n > 0
ensures Factorial(n) == n * Factorial(n - 1)
Axiomatizing recursive functions is error prone
ghost function Factorial(n: int): int
lemma FactorialBogus1(n: int)
ensures Factorial(n) == n * Factorial(n - 1)
lemma FactorialBogus2(n: int)
ensures Factorial(n) == n * Factorial(n)
Instead, the function can be defined
ghost function Factorial(n: int): int
requires 0 <= n
{
if n == 0 then 1 else Factorial(n - 1)
}
In most cases, the termination metric is obvious and Dafny can infer it
function Sum(n: int): int
decreases n // this line can be omitted, because Dafny will infer it
{
if n <= 0 then 0 else Sum(n - 1)
}
If not, you can give it a termination metric using a decreases
clause
function SumUpTo(counter: int, upTo: int): int
decreases upTo - counter // here, the decreases clause is needed; Dafny will not infer it
{
if upTo <= counter then
counter
else
counter + SumUpTo(counter + 1, upTo)
}
The decrease
clause is an expression that decreases at every recursive call
There are functions that exist and that are not well-founded
predicate Closure<T(!new)>(Relation: (T, T) -> bool, x: T, y: T)
lemma ClosureProp1<T(!new)>(Relation: (T, T) -> bool, x: T)
ensures Closure(Relation, x, x)
lemma ClosureProp2<T(!new)>(Relation: (T, T) -> bool, x: T, y: T)
ensures exists z: T :: Relation(x, z) && Closure(Relation, z, y)
Such a function can be declared as a least predicate
least predicate Closure<T(!new)>(Relation: (T, T) -> bool, x: T, y: T) {
|| x == y
|| (exists z: T :: Relation(x, z) && Closure(Relation, z, y))
}
In this case, Dafny ensures that the function actually exists based on its type and body
You will rarely need this for routine program verification
When we axiomatize a theory, we posit the existence of types, constants, functions, …
type Peano(0)
ghost const Zero: Peano
ghost function Succ(n: Peano): Peano
lemma PeanoInjectivity(m: Peano, n: Peano)
requires Succ(m) == Succ(n)
ensures m == n
lemma PeanoDiff(n: Peano)
ensures Succ(n) != Zero
lemma PeanoExhaustive(n: Peano)
ensures n == Zero || exists m: Peano :: n == Succ(m)
lemma InductionPrinciple(P: Peano -> bool)
requires P(Zero)
requires forall n: Peano :: P(n) ==> P(Succ(n))
ensures forall n: Peano :: P(n)
Algebraic datatypes are a powerful tool to summarize all this in one definition
datatype Peano =
| Zero
| Succ(Peano)