Formal Software Verification

6.7. Worked Examples🔗

Each example below is carried out in full and verbalised. They are disjoint from the section examples and the exercises, and Lean checks every line when the notes are built.

6.7.1. Arithmetic expressions and their evaluation🔗

An arithmetic expression is a constant, a variable, a sum, or a product, and this is an inductive type with four constructors, two of them recursive. An evaluator takes an environment giving a value to each variable and computes the value of an expression by recursion on its structure.

namespace Func inductive AExp where | const (i : ) | var (x : String) | add (a b : AExp) | mul (a b : AExp) def eval (env : String ) : AExp | .const i => i | .var x => env x | .add a b => eval env a + eval env b | .mul a b => eval env a * eval env b end Func

The expression below reads 2 + x × 5, and under an environment giving x the value 3 it evaluates to 17.

namespace Func def sampleEnv : String := fun s => if s = "x" then 3 else 0 def e1 : AExp := .add (.const 2) (.mul (.var "x") (.const 5)) 17#eval eval sampleEnv e1 end Func
17

The defining equation for a sum gives the law eval env (add a b) = eval env a + eval env b, and it holds by computation, since the equation is exactly the recursion step.

namespace Func example (env : String ) (a b : AExp) : eval env (.add a b) = eval env a + eval env b := rfl end Func

These expressions are the syntax of a small language, and the environment is its state. The operational semantics of the Hoare-logic weeks builds on exactly this shape.

6.7.2. A type class for size🔗

The Size class of §6.5 extends to trees with one more instance, and a function then measures a whole list of sized values. Resolution supplies the tree instance for each element and the list structure drives the recursion.

namespace Func instance {α : Type} : Size (Tree α) where size := treeSize def totalSize {α : Type} [Size α] : List α | [] => 0 | x :: xs => Size.size x + totalSize xs end Func

The list below holds a tree and its mirror, each of size 2, so the total is 4.

namespace Func 4#eval totalSize [t1, mirror t1] end Func
4

The function requests [Size α] once, and resolution finds the tree instance because the elements are trees. The Membership instance of Lecture 2 and the associativity instance of Lecture 4 are the same mechanism seen plainly, an operation attached to a type and found by its type.

6.7.3. A record with an extension🔗

A record collects related fields under one name, and extends builds a specialised record on a general one. An account has an owner and a balance, and a named account adds a nickname while keeping both inherited fields.

namespace Func structure Account where owner : String balance : structure NamedAccount extends Account where nickname : String def acc : NamedAccount := { owner := "A", balance := 100, nickname := "main" } example : acc.owner = "A" := rfl example : acc.balance = 100 := acc.balance = 100 All goals completed! 🐙 end Func

Building the record with the field syntax and projecting a field returns that field, by computation. The single constructor of a structure is the same idea as the And.intro of Lecture 1, one constructor gathering several arguments, with the fields named instead of positional.

6.7.4. Mirroring a tree🔗

The mirror of §6.6 swaps the subtrees at every branch, so mirroring twice should return the original tree. On a closed tree this holds by computation.

namespace Func example : mirror (mirror t1) = t1 := rfl end Func

The general law mirror (mirror t) = t, for every tree t, is not a computation. It needs structural induction, with the recursive calls of mirror supplying the induction hypotheses for the two subtrees, and it is the first worked example of Lecture 7. This closes the loop with Lecture 5's reverse_reverse, which proved the list analogue by recursion, and it sets up the induction to come.