Definition
Dafny
Dafny is an object-oriented and functional programming language designed for formal verification. It enables developers to write code alongside its specification (preconditions, postconditions, invariants), which the compiler then proves correct using an automated theorem prover. 1
Verification Semantics
Partial vs. Total Correctness
Partial Correctness
A program is partially correct with respect to a precondition and post-condition if whenever execution starts in a state satisfying and terminates, the final state satisfies .
Total Correctness
A program is totally correct if it is partially correct and terminates for all inputs satisfying .
Dafny proves Total Correctness by default.
Examples:
- Valid/Valid:
(Terminates and holds). - Valid/Invalid:
(Infinite loop implies partial correctness vacuously, but fails total correctness). - Valid/Valid:
(Vacuously true for both as precondition is impossible).
Opacity
Opacity: Functions vs. Methods
Would the verification behaviour change if a method is changed to a function?
Answer: Yes.
- Methods are opaque, meaning the callers takes only explicitly defined contracts into account.
- Functions, on the other hand, are transparent to the caller. The caller takes the actual implementation of the function into account.
Proving: Functions vs. Methods
Can it be the case that Dafny proves a program correct using a method contract, but does not when using a function with the same implementation as the method?
Answer: Yes. Dafny’s function provider might fail for more complex functions due to timeouts. Methods are faster to check since they only rely on the explicitly specified contract.
Termination
To prove total correctness, Dafny must verify that every loop and recursive call terminates. It does this using a variant (or rank function).
Decreases Clause
The decreases clause specifies the rank function. Dafny checks two properties:
- Strict Decrease: The rank must be strictly smaller in the recursive call or next iteration.
- Bounded: The rank must be bounded from below (cannot decrease forever).
Valid Ranks
Dafny supports various types for the decreases clause:
- Integers: Must decrease and be bounded by .
- Sequences/Sets: The size (cardinality) must decrease.
- Tuples: Compared lexicographically.
If no clause is provided, Dafny attempts to guess one (e.g.,
xifx < Nis the guard).
Lexicographical Ordering
For multiple parameters, use tuples. Dafny compares these using lexicographical order ().
Tuple Decrease Logic
For a tuple :
- Primary Measure (): If decreases, the tuple is smaller immediately.
- Secondary Measure (): If stays the same, must decrease.
Example (Ackermann-like):
method Ackermann(m: int, n: int) returns (r: int)
requires m >= 0 && n >= 0
decreases m, n // Uses tuple (m, n)
{
if m == 0 {
r := n + 1;
} else if n == 0 {
r := Ackermann(m - 1, 1); // m decreases
} else {
var t := Ackermann(m, n - 1); // n decreases, m same
r := Ackermann(m - 1, t); // m decreases
}
}Examples:
1. Simple Upper Bound
The function F increases x until it hits 10. The distance to 10 (10 - x) decreases with every step.
function F(x: int): int
decreases 10 - x
{
if x > 10 then 0
else 2*F(x+1)
}2. Relative Difference
The function G decreases the gap between x and y (x - y) until x <= y.
function G(x: int, y: int): int
decreases x - y
{
if x <= y then 0
else 1+G(x-1, y)
}3. Magnitude Sum
The function H terminates when x and y have the same sign. The sum of absolute values decreases because the variables move towards zero or flip signs in a way that reduces total magnitude.
function H(x: int, y: int): int
decreases Abs(x) + Abs(y)
{
if x * y >= 0 then
0
else if y < 0 then
H(x-1, y+2)
else
H(y-1, x+2) // Note: the arguments are swapped here
}Loops
Dafny reasons about loops solely through guards and loop invariants.
Loop Opacity
Dafny treats loops as opaque. To reason about the state after a loop, Dafny uses only:
- The Loop Invariant is true.
- The Loop Guard is false.
Thus, the effective postcondition of the loop is:
Verification Logic
To verify while (G) invariant I { Body }, three conditions must hold:
-
Initialisation ()
The invariant holds before the loop starts. -
Preservation ()
If the invariant and guard hold, executing the body preserves the invariant. -
Termination ()
When the loop finishes (guard false), the invariant implies the post-condition.
Example:
To prove reaches :
- Guard:
- Update:
- Invariant:
Specification
Strong Contracts?
Should you always aim to make your post-conditions as strong as possible?
Answer: No, Strong post-conditions lead to over-specification and loss of abstraction. Modifying code becomes increasingly difficult.
Design Principles
Designing contracts involves a trade-off between the client (caller) and the supplier (method):
Strong vs. Weak Preconditions:
- Weak Precondition:
- Pros: Easier for the Client to use (fewer restrictions).
- Cons: Harder for the Method to implement (must handle more cases, e.g., nulls, negatives).
- Strong Precondition:
- Pros: Easier for the Method (can assume valid data).
- Cons: Harder for the Client (must ensure data is valid before calling).
Runtime Checking
While Dafny verifies contracts statically (at compile time), standard languages (Java, Python) check contracts at runtime using:
- Error Output: Printing error messages (weakest).
- Exceptions: Throwing
IllegalArgumentException(standard). - Assertions: Using
assertstatements (often disabled in production).
Class
Dafny supports object-oriented programming with classes. Unlike datatypes, classes are reference types and contain mutable state.
Class Invariant
A class invariant is a condition that defines the valid state of an object. It is typically defined as a ghost predicate.
Invariant Maintenance
Dafny does not automatically enforce class invariants. You must explicitly:
- Establish the invariant in the constructor (
ensures Valid()).- Require it in methods (
requires Valid()).- Preserve it in methods (
ensures Valid()).
Example (Ledger):
class Ledger {
var total_credit: nat
var total_debit: nat
// Class Invariant
ghost predicate Balanced()
reads this
{
this.total_credit == this.total_debit
}
constructor()
ensures Balanced() // establish invariant
{
this.total_credit := 0;
this.total_debit := 0;
}
method RecordTransactions(newDebit: array<nat>, newCredit: array<nat>)
modifies this
requires Balanced() // require valid state
ensures Balanced() // preserve valid state
requires Sum(newDebit) == Sum(newCredit)
{
this.total_debit := this.total_debit + Sum(newDebit);
this.total_credit := this.total_credit + Sum(newCredit);
}
}
function Sum(a: array<nat>): int
reads a { SumFrom(a,0) }
function SumFrom(a: array<nat>, pos: nat): nat
decreases a.Length - pos
reads a
{
if pos >= a.Length then 0 else a[pos] + SumFrom(a, pos+1)
}Ghost Code
The
ghostkeyword marks code that is used only for verification. It is erased by the compiler and does not exist at runtime.
- Usage: Specification functions, logical variables, proofs.
- Restriction: Ghost code cannot affect non-ghost (compiled) state.
Subtyping
Classes in Dafny follow strict behavioural subtyping (Design by Contract). When a subclass overrides a method, it must satisfy the Liskov Substitution Principle:
- Preconditions are contravariant: Sub-classes can require less (weaken), but not more.
- Postconditions are covariant: Sub-classes can promise more (strengthen), but not less.
Base Trait:
trait Course {
var ECTS: nat
// The Contract (The Baseline)
method Complete(effort: int) returns (grade: int)
modifies this
requires effort >= 25 * ECTS
ensures 1 <= grade <= 5
}1. RegularCourse (Valid)
- Pre: Weakened () Valid.
- Post: Strengthened () Valid.
class RegularCourse extends Course {
method Complete(effort: int) returns (grade: int)
modifies this
requires effort >= 20 * ECTS
ensures 0 < grade <= 3
{ return 2; }
}2. HardCourse (Invalid)
- Pre: Strengthened () Error. Subclass demands more effort than parent.
class HardCourse extends Course {
method Complete(effort: int) returns (grade: int)
modifies this
requires effort >= 50 * ECTS // ERROR
ensures 2 <= grade <= 4
{ return 3; }
}3. EasyCourse (Invalid)
- Pre: Strengthened (Range restricted to ) Error. Rejects valid inputs ().
class EasyCourse extends Course {
method Complete(effort: int) returns (grade: int)
modifies this
requires effort <= 10 * ECTS // ERROR
ensures grade == 1
{ return 1; }
}4. ImpossibleCourse (Valid)
- Pre: Weakened (True) Valid. Accepts any effort.
- Post: Strengthened (Exactly 5) Valid.
class ImpossibleCourse extends Course {
method Complete(effort: int) returns (grade: int)
modifies this
// Implicit 'requires true'
ensures grade == 5
{ return 5; }
}5. NeverEndingCourse (Valid)
- Pre: Weakened (True) Valid.
- Post: Strengthened (False) Valid. The empty set is a subset of .
class NeverEndingCourse extends Course {
method Complete(effort: int) returns (grade: int)
modifies this
ensures false
{
while true {} // Divergence satisfies 'ensures false'
}
}6. GermanCourse (Invalid)
- Post: Weakened () Error. Promises less than parent (might return 6).
class GermanCourse extends Course {
method Complete(effort: int) returns (grade: int)
modifies this
requires effort >= 25 * ECTS
ensures 1 <= grade <= 6 // ERROR
{ return 6; }
}7. NonExistentCourse (Invalid)
- Pre: Strengthened (False) Error. Accepts nothing, violating parent contract.
class NonExistentCourse extends Course {
method Complete(effort: int) returns (grade: int)
modifies this
requires false // ERROR
{ return 1; }
}