Instructor: Jianan Yao

LEC: Th1:10-3:00PM MC252

assn*4 40%
project 50%

Week 1. Sep 10

types of program synthesis

formal verification: use formal methods of math to verify a program satisfying its specs.

timeline of major verification tools and systems

Flashfill: excel feature to autofill cells based on examples.

Alan Turing       AT
John McCarthy     JM    
Donald E. Knuth   DEK
Edsger Dijstra    // guess ED
Leslie B. Lamport

suffers from ambiguity (too few examples - if keep 1st cell only, then it may guess JMC).

eg. given input: (425)-706-7709, output: 425-706-7709. for simplicity, restrict the synthesized program to be a finite substring concat:

output = substring + constant + substring + ...

for "706" there are many possible expressions deriving it:

suppose there are 10 different ways to derive "425" in the output, 10 ways for "706" and 10 ways for "7709". there are 1000 candidate programs. use a compact set representation instead of enumerating all:

[1st number between 1st pair of "()": "425"], ...
"-"
[2nd number between 1st and 2nd "-": "706"], ...
"-"
[3rd number after 2nd "-": "7709"], ...

if there are more examples, the synthesized program must satisfy all. so we need to intersect the candidate sets for each part of the output.

this however assumes a linear structure (split by "-"). to improve we use a DAG representation.

        ┌────────────────────┐             ┌────────────────────┐             ┌────────────────────┐
        │ 1st number between │   ┌─────┐   │ 2nd number between │   ┌─────┐   │ 3rd number         │
start ●─┤ 1st pair of "()"   ├─●─┤ "-" ├─●─┤ 1st and 2nd "-"    ├─●─┤ "-" ├─●─┤ 1st number after   ├─● end
        │ "425"              │ │ └─────┘ │ │ "706"              │   └─────┘   │ last "-"  "7709"   │ │
        │ ...                │ │         │ │ ...                │             │ ...                │ │
        └────────────────────┘ │         │ └────────────────────┘             └────────────────────┘ │
                               │         │             ┌────────────────────┐                        │
                               │         │             │ after 1st "-"      │                        │
                               │         └─────────────┤ last 8 characters  ├────────────────────────┤
                               │                       │ "706-7709"         │                        │
                               │                       │ ...                │                        │
                               │                       └────────────────────┘                        │
                               │                       ┌────────────────────┐                        │
                               └───────────────────────┤ after first ")"    ├────────────────────────┘
                                                       │ ...                │
                                                       └────────────────────┘

what if no single program works for every example? the intersections of DAGs can be empty.

(425)-706-7709  425-706-7709
604 910 2026    604-910-2026
978-5804        416-978-5804
946 8727        416-946-8727

FlashFill greedily groups examples that share a common program. then synthesize a classifier (e.g. boolean) to separate examples. The final output:

if input has 3 numbers
then SubStr(input, 1st number) + “-” + SubStr(input, 2nd number) + “-” + SubStr(input, 3rd number)
else “416-” + SubStr(input, 1st number) + “-” + SubStr(input, 2nd number)

Week 2. Sep. 17

correctness: starting from any state satisfying P (precondition), we want to ensure running this code lands in a state satisfying Q (postcondition).

pre- and postconditions divide responsibilities between caller and callee

caller callee
precondition must establish may assume
postcondition may assume must establish

consequences:

eg. specifying min

// pre: true
int min(int a, int b) {
    if (a < b) r = a;
    else r = b;
    return r;
}
// post: r <= a && r <= b && (r == a || r == b)

central trade of formal verification: burden of being right moves from a large operational artifact (the code) to a small declarative one (the specification). specs are usually far shorter and far easier to review, but they are now the thing that must be right.

defn. (hoare triple) { P } C { Q }, where P is the precondition (an assertion), C is the program (command), and Q is the postcondition (an assertion).

notation.

hoare logic is sound: anything derivable is true. It is also complete relative to the assertion language (Cook, 1978) - if a triple is true, a derivation exists, provided we can prove the arithmetic facts it needs.

notation.

eg. with unbounded integers

we use one rule per construct.

defn. (skip axiom)

{P} skip {Q}\frac{}{\{P\} \textrm{ skip } \{Q\}}

defn. (assignment axiom)

{Q[e/x]} x:=e {Q}\frac{}{\{Q[e/x]\}\ x:=e\ \{Q\}}

Q[e/x] means: Q with every free occurrence of x replaced by the expression e.

side conditions that matter in real language:

eg. we read assignment backwards. to guarantee Q after the assignment, require beforehand what Q says about x.

postcond Q command x:=e precond P[e/x]
x>0 x:=y+1 y+1>0
x=6 x:=x+1 x+1=6 i.e. x=5
y*y>=36 y:=x+5 (x+5)*(x+5) >= 36
x=y x:=x+1 x+1=y

eg. the following forward rule is incorrect: {P} x:=e {P∧x=e}. take P as x=5 and command as x:=x+1. the post cond yields x=5 ∧ x=x+1, which is false.

there is a correct forward rule (sound, but introduces an existential quantifier):

{P} x:=e {∃x0.P[x0/x]∧x=e[x0/x]}\{P\}\ x:=e\ \{\exists x_0.P[x_0/x]\land x=e[x_0/x]\}

defn. (sequence rule)

{P} C1 {R}   {R} C2 {Q}{P} C1;C2 {Q}\frac{\{P\}\ C_1\ \{R\}\ \ \ \{R\}\ C_2\ \{Q\}}{\{P\}\ C_1;C_2\ \{Q\}}

we may always strengthen the precondition and weaken the postcondition (conseq rule):

P⇒P′   {P′} C {Q′}   Q′⇒Q{P} C {Q}\frac{P\Rightarrow P'\ \ \ \{P'\}\ C\ \{Q'\}\ \ \ Q'\Rightarrow Q}{\{P\}\ C\ \{Q\}}

defn. (conditional rule)

{P∧b} C1 {Q}   {P∧¬b} C2 {Q}{P} if B then C1 else C2 {Q}\frac{\{P\land b\}\ C_1\ \{Q\}\ \ \ \{P\land \neg b\}\ C_2\ \{Q\}}{\{P\}\ \text{if } B \text{ then } C_1 \text{ else } C_2\ \{Q\}}

eg. show ⊢{x≥1}   y:=x+5;z:=y⋅y   {z≥36}\vdash\{x\geq1\}\ \ \ y:=x+5;z:=y\cdot y\ \ \ \{z\geq36\}.

  1. assign on z:=y*y with Q={z≥36}:
    {y⋅y≥36}   z:=y⋅y   {z≥36}\{y\cdot y\geq36\}\ \ \ z:=y\cdot y\ \ \ \{z\geq36\}
  2. assign on y:=x+5 with Q={y*y≥36}:
    {(x+5)⋅(x+5)≥36}   y:=x+5   {y⋅y≥36}\{(x+5)\cdot (x+5)\geq36\}\ \ \ y:=x+5\ \ \ \{y\cdot y\geq36\}
  3. seq, taking R={y*y≥36}:
    {(x+5)⋅(x+5)≥36}   y:=x+5;z:=y⋅y   {z≥36}\{(x+5)\cdot (x+5)\geq36\}\ \ \ y:=x+5;z:=y\cdot y\ \ \ \{z\geq36\}
  4. conseq, which requires x≥1 ⇒ (x+5)*(x+5)≥36.
    {x≥1}   y:=x+5;z:=y⋅y   {z≥36}\{x\geq1\}\ \ \ y:=x+5;z:=y\cdot y\ \ \ \{z\geq36\}

steps 1-3 are mechanical, step 4 is the only genuine obligation (and it is not related to program).

eg. show ⊢{true}   if (a<b) r:=a else r:=b   {Q}\vdash \{\text{true}\}\ \ \ \text{if}\ (a<b)\ r:=a\ \text{else}\ r:=b\ \ \ \{Q\}, where Q≡r≤a∧r≤b∧(r=a∨r=b)Q\equiv r\leq a\land r\leq b\land (r=a\lor r=b).

observation:

plan:

  1. compute the weakest assertion that must hold before C to guarantee Q (weakest precondition)
  2. emit the one implication remaining (verification condition)
  3. hand that implication to an automatic prover

defn. wp(C, Q) is the weakest assertion P such that {P} C {Q} holds, meaning:

  1. {wp(C, Q) C {Q} holds and,
  2. for every P', with {P'} C {Q}, we have P' ⇒ wp(C, Q).

if our actual precondition P does not imply wp, non proof {P} C {Q} exists. there single check P ⇒ wp(C, Q) is therefore sound and complete.

note. under partial correctness, this is strictly the weakest liberal precondition (wlp), which does not require termination. for loop-free programs, wp=wlp.

expression defn.
wp(skip, Q) Q
wp(x:=e, Q) Q[e/x]
wp(C₁;C₂, Q) wp(C₁, wp(C₂, Q))
wp(if b then C₁ else C₂, Q) (b ⇒ wp(C₁, Q)) ∧ (¬b ⇒ wp(C₂, Q))
wp(assert R, Q) R ∧ Q
wp(assume R, Q) R ⇒ Q

eg. Q={z≥36}, C={y=x+5; z=y*y}, find wp(C, Q).

  // {(x + 5) * (x + 5) >= 36} <-- wp(C, Q)
y = x + 5;
  // {y * y >= 36} <-- wp(z := y*y, Q)
z = y * y;
  // {z >= 36} <-- Q

start from bottom with Q, and push it upwards one statement at a time. we end up at the top is a formula over program's input only.

caveat: by substitution, we may have duplicate large expressions. real tools avoid blow-up with SSA form and shared term representations.

defn. given spec (P, Q) and loop-free program C, the verification condition is a single formula: VC(P, C, Q) ≡ P ⇒ wp(C, Q).

for our example, it is x≥1 ⇒ (x+5)*(x+5)≥36.

verification condition generation:

  1. given program C, spec P, Q
  2. use VC generator to create wp, SSA (knows the programming language)
  3. get the verification condition formula
  4. use automatic prover (only knows logic)

the verification condition is the interface separating compilation and search.

eg. given Q≡r≤a∧r≤b∧(r=a∨r=b)Q\equiv r\leq a\land r\leq b\land(r=a\lor r=b), then

wp(if a<b then r:=a else r:=b,Q)=(a<b⇒Q[a/r])∧(¬(a<b)⇒Q[b/r])=(a<b⇒(a≤a∧a≤b∧(a=a∨a=b)))∧(¬(a<b)⇒(b≤a∧b≤b∧(b=a∨b=b)))\begin{align*} &\mathrm{wp}(\text{if }a<b\text{ then }r:=a\text{ else }r:=b, Q)\\ ={}&(a<b\Rightarrow Q[a/r]) \land (\neg(a<b)\Rightarrow Q[b/r])\\ ={}&(a<b\Rightarrow (a\leq a\land a\leq b\land(a=a\lor a=b))) \\ &\land (\neg(a<b)\Rightarrow (b\leq a\land b\leq b\land(b=a\lor b=b)))\\ \end{align*}

and VC ≡ true ⇒ formula above.

note if there are n conditionals, then there are 2^n paths to consider. VC has joined the implications so it grows linearly. case analysis is moved inside the solver.

defn. a formula is valid if it is true under every interpretation of its free variables. a formula is satisfiable if it is true under some interpretation.

given a VC, we ask whether ¬VC is satisfiable:

eg. if F = x≥1 ⇒ (x+5)²≥36, then ¬F = x≥1 ∧ (x+5)²<36. we feed this into solver.

SAT: given boolean formula F over variables x₁, x₂, ..., xₙ, find an assignment of true/false to those variables making F true, or prove that none exists

satisfiability modulo theories (SMT): given a first-order formula F in which some function and predicate symbols carry a fixed interpretation prescribed by a background theory T, decide whether F is satisfiable in T.

common theories:

theory example decidable? cost
equality with uninterpreted functions (EUF) f(f(f(a))) = a ∧ f(f(f(f(f(a))))) = a ∧ f(a) ≠ a — unsat yes polynomial - congruence closure
linear integer arithmetic (LIA) 3x+2y≤7 ∧ x−y≥1 yes NP-complete
nonlinear integer arithmetic (NIA) x≥1 ∧ (x+5)²<36 no undecidable - Hilbert's 10th problem
linear real arithmetic (LRA) x/2+y≤3.5 ∧ x≥0 yes polynomial; simplex in practice
nonlinear real arithmetic x²+y²=4 yes (Tarski 1951) doubly exponential - CAD
first-order logic with quantifiers ∀x.∃y.f(x)=y no undecidable - Church, Turing 1936
fixed-width bit-vectors (BV) x & (x − 1) = 0 ∧ x ≠ 0 (x is a power of two) yes NP-complete - bit-blasted to SAT
arrays (with EUF) select(store(A, i, v), j) yes NP-complete
strings concat(s, " world") = "hello world"

undecidable means no procedure works for all input, but there may be that works for some.

how solvers work:

using Z3:

from z3 import *

x = Int('x')
pre  = x >= 1
post = (x + 5) * (x + 5) >= 36    # this is wp(C, z >= 36)
s = Solver()
s.add(pre, Not(post))  # assert negation of VC
s.check()  # unsat => correct
# or prove(Implies(pre, post)) 

# using SSA
x, y, z = Ints('x y z')
s = Solver()
s.add(x >= 1)
s.add(y == x + 5)
s.add(z == y * y)
s.add(Not(z >= 36))
s.check()  # unsat => correct

# incorrect
s.add(z==y+y)
s.check()  # sat => incorrect
s.model()  # [y = 6, x = 1, z = 12]

# min function
a, b, r = Ints('a b r')
s = Solver()
s.add(r == If(a < b, a, b))  # program
s.add(Not(And(r <= a, r <= b, Or(r == a, r == b))))  # negated postcond
s.check()  # unsat  =>  correct

Week 3. Sep 24

suppose we have a while loop while b do C, we can unwrap it once:

while b do C=if b then (C;while b do C) else skipwp(while b do C,Q)=(¬b∧Q)∨( b∧wp(C,wp(while b do C,Q)))\begin{align*} \text{while }b \text{ do } C &= \text{if } b \text{ then } (C; \text{while } b \text{ do } C) \text{ else skip}\\ \mathrm{wp}(\text{while } b \text{ do } C, Q) &= (\neg b\land Q)\lor (\ b \land \mathrm{wp}(C, \mathrm{wp}(\text{while } b \text{ do } C, Q))) \end{align*}

the math solution is its fixed point. we can also unwrap it k times: this is bounded model checking (bug-finding technique, not a proof). idea is to find an assertion every step.

defn. a loop invariant is an assertion I attached to the top to the loop.

defn. (while rule)

{I∧b}   C   {I}{I}   while b do C   {I∧¬b}\frac{\{I\land b\}\ \ \ C\ \ \ \{I\}}{\{I\}\ \ \ \text{while }b\text{ do }C\ \ \ \{I\land\lnot b\}}

I is not part of program. proof writer has to find it.

proof obligations:

obligation prove meaning
establishment P ⇒ I I holds when loop enters
preservation I ∧ b ⇒ wp(C, I) one iteration keeps I true
exit I ∧ ¬b ⇒ Q I is strong enough to give the postcond

eg. summing 0 to n-1

// pre: n >= 0
i = 0;
s = 0;
// inv: 0 <= i <= n
//   && 2 * s == i * (i - 1)
while (i < n) {
    s = s + i;
    i = i + 1;
}
// post: 2 * s == n * (n - 1)

i <= n so that the loop exit is covered.

from z3 import *
i, s, n, i2, s2 = Ints('i s n i2 s2')
 
def Inv(i, s): return And(0 <= i, i <= n, 2*s == i*(i-1))

# establishment: n≥0 ⇒ I(i=0,s=0)
prove(Implies(n >= 0, Inv(0, 0)))
# preservation: I(i,s) ∧ i<n ⇒ I(i+1,s+i)
prove(Implies(And(Inv(i, s), i < n,
                  s2 == s + i, i2 == i + 1), Inv(i2, s2)))
# exit: I(i,s) ∧ ¬(i<n) ⇒ 2*s == n*(n-1)
prove(Implies(And(Inv(i, s), Not(i < n)), 2*s == n*(n-1)))

eg. finding maximal element in array

// pre:  n >= 1
m = A[0];
i = 1;
// inv:  1 <= i <= n
//    && (∀ k. 0 <= k < i ==> A[k] <= m)
//    && (∃ k. 0 <= k < i && A[k] == m)
while (i < n) {
    if (A[i] > m)  m = A[i];
    i = i + 1;
}
// post: (∀ k. 0 <= k < n ==> A[k] <= m)
//    && (∃ k. 0 <= k < n && A[k] == m)

eg. invariant patterns

postcondition invariant I
2*s = n(n-1) 2*s = i*(i-1) ∧ 0≤i≤n
∀k<n. A[k]≤m ∀k<i. A[k]≤m ∧ 1≤i≤n
r=n! r=i! ∧ 0≤i≤n
A[0..n) is sorted A[0..i) is sorted ∧ 1≤i≤n

invariant vs. inductive variant

defn. a counterexample to induction (CTI) is a state satisfying I ∧ b but its successor violates I.

eg. true but not inductive

// pre: True
x = 0;
// inv: x <= 10
while (x < 10) {
    x = x + 2;
}
// post: x == 10

the invariant is true, but setting x=9 yields false (we have x≤10 ∧ x<10 !⇒ x+2≤10), although x=9 never happens. strengthening it to x≤10 ∧ x%2=0 can go through.

the strengthen loop algo:

eg. which are inductive invariants?

// pre:  true
x = 0;
y = 0;
while (x < 10) {
    x = x + 1;
    y = y + 2;
}
// post: y == 20

check establish, preserve, and postcond

when a check fails: three diagnoses

the check that fails usual cause what to do
establishment P ⇒ I I is too strong weaken I, or strengthen the precondition
preservation I ∧ b ⇒ wp(C, I) I is too weak — or the body really is wrong read the CTI and decide which
exit I ∧ ¬b ⇒ Q I is too weak for Q add what the postcondition needs

only when a preservation check fails with a reachable CTI can it indicate a bug in the program.

eg. nested loops: each loop needs its own invariant.

// inv_outer: A[0..i) is sorted && 0<=i<=n
while (i < n) {
    j = i;
    // inv_inner: ... && 0<=j<=i
    while (j > 0 && A[j-1] > A[j]) {
        swap(A, j-1, j);
        j = j - 1;
    }
    i = i + 1;
}

finding invariant automatically:

eg. a partially correct program

// pre:  n >= 0
i = 0;
// inv:  0 <= i
while (i != n) {
    i = i + 2;
}
// post: i == n

defn. a ranking function (variant) for a loop is an integer expression V over the program such that

together they force termination. ℕ is common choice but anything without infinite descending chains works (e.g. tuples, structural orders).

defn. (while-total rule)

I∧b⇒V≥0   [I∧b∧V=v0]   C   [I∧V<v0][I]   while b do C   [I∧¬b]\frac{I\land b\Rightarrow V\geq0\ \ \ [I\land b\land V=v_0]\ \ \ C\ \ \ [I\land V<v_0]}{[I]\ \ \ \text{while }b\text{ do }C\ \ \ [I\land\lnot b]}

called invariant and decreases clauses in Dafny and Verus.

eg. ranking functions

loop ranking function
while (i<n) { i=i+1; } n-i
while (x>0) { x=x-3; } x
while (lo<hi) { ... lo=mid+1; ... hi=mid; } hi-lo
while (x>0 && y>0) { if(...) x--; else y--; } x+y
while (i!=n) { i=i+2; } none - diverges for odd n
while (n!=1) { n = even(n)?n/2:3*n+1; } unknown - Collatz conjecture

eg. lexicographic ranking function

// pre: x >= 0, y >= 0, N >= 0
while (x > 0) {
    if (y > 0) {
        y = y - 1;
    } else {
        x = x - 1;
        y = N;
    }
}

termination in practice:

the complete picture:

program C   ─┐
spec P,Q    ─┼─► VC generator ─► verif. conditions ─► SMT solver ─► verified/counterexample/unknown
inv+ranking ─┘

what can go wrong:

Week 4. Oct 1

verus is SMT-based verifier based on Z3 solver as backend.

why rust? ownership simplifies proof:

fn set_both(p: &mut u32, q: &mut u32)
  ensures
    *final(p) == 1,
    *final(q) == 2,
{
  // p and q must be different
  *p = 1;
  *q = 2;
}
vstd::prelude::*;

verus! {
fn min(a: u64, b: u64) -> (r: u64)
  ensures
    r <= a && r <= b,
    r == a || r == b
{
  if a < b { a } else { b }
}
} // verus!

two paths for program:

      ┍ erase ghost code ──► rustc ──► exec
      │ (spec, pf, ...)
code ━┥
      │ mode check ──► verification cond. ──► Z3
      ┕ VIR->AIR          SMT-LIB             sat?
mode written as compiled? checked? used for
exec fn yes no program
proof proof fn, proof {} no yes lemmas and pf steps
spec spec fn no yes math definitions and specs

ghost code may use math types: int, nat (unbounded), Seq, Set, Map. they have no machine repr and exist only for verification.

specs:

spec Verus
{P} C {Q} fn f(...) -> (r: T) requires P ensures Q { C }
P ⇒ Q P ==> Q
∀ i. P(i) forall|i: int| P(i)
∃ i. P(i) exists|i: int| P(i)
loop invariant I, ranking function V while b invariant I decreases V { ... }
an assertion at some program point assert(P);

spec is mathematical:

fn avg(a: u32, b: u32) -> (r: u32)
  ensures r == (a + b) / 2,
{
  // a / 2 + b / 2 + (a % 2 + b % 2) / 2
  (a + b) / 2
}
// error: may overflow

caller sees only the contract:

fn min(a: u64, b: u64) -> (r: u64)
  ensures r <= a && r <= b,
{
  if a < b { a } else { b }
}

fn caller() {
  let m = min(3, 7);
  assert(m <= 3); // verified
  assert(m == 3); // fails (need to add another ensures)
}

spec functions:

spec fn is_even(x: int) -> bool {
  x % 2 == 0
}

// 1 + 2 + ... + n
spec fn triangle(n: nat) -> nat
  decreases n,
{
  if n == 0 { 0 }
  else { n + triangle((n - 1) as nat) }
}

mutable states:

struct Counter { count: u64, limit: u64 }
impl Counter {
  spec fn wf(&self) -> bool {
    self.count <= self.limit
  }

  fn inc(&mut self)
    requires
      old(self).wf(),
      old(self).count < old(self).limit,
    ensures
      final(self).wf(),
      final(self).count == old(self).count + 1,
      final(self).limit == old(self).limit,
  {
    self.count = self.count + 1;
  }
}

loop:

fn ten() -> (x: u32)
  ensures x == 10,
{
  let mut x: u32 = 0;
  while x < 10
    invariant
      x <= 10,
      x % 2 == 0,
    decreases 10 - x,
  {
    x = x + 2;
  }
  x
}

queries:

; function with loop cut
assume x == 0            ; P ⇒ I 
assert x <= 10           ; before loop
assert x % 2 == 0
havoc x                  ; forget x ...
assume x <= 10           ; ... keep I
assume x % 2 == 0
assume !(x < 10)         ; loop has exited
assert x == 10           ; postcondition

; one iteration of loop body
assume x <= 10           ; I holds
assume x % 2 == 0
assume x < 10            ; guard holds
assert 0 <= x + 2 < 2^32 ; overflow check
x := x + 2
assert x <= 10           ; I preserved
assert x % 2 == 0
assert decreased(10 - x) ; termination

inside loop body, verifier knows only the invariant and loop condition:

fn f(n: u64)
  requires n > 5,
{
  let x: u64 = 7;
  let mut i: u64 = 0;
  while i < 3
    invariant i <= 3,
    decreases 3 - i,
  {
    // assert(n > 5); // fails
    // assert(x == 7); // fails
    i = i + 1;
  }
  assert(n > 5); // verified
  assert(i == 3); // verified
}

if body needs a fact, put in invariant.

Seq:

with quantifier:

fn max(v: &Vec<u64>) -> (m: u64)
  requires v.len() >= 1,
  ensures
    forall|k: int| 0 <= k < v.len() ==> v[k] <= m,
    exists|k: int| 0 <= k < v.len() && v[k] == m,
{
  let mut m = v[0];
  let mut i: usize = 1;
  while i < v.len()
    invariant
      1 <= i <= v.len(),
      forall|k: int| 0 <= k < i ==> v[k] <= m,
      exists|k: int| 0 <= k < i && v[k] == m,
    decreases v.len() - i,
  {
    if v[i] > m { m = v[i]; }
    i = i + 1;
  }
  m
}

why quantifiers are hard:

where what solver must do difficulty
∀ in the goal prove P(c) for a fresh, arbitrary c easy
∃ in a hypothesis give the witness a fresh name c, assume P(c) easy
∀ in a hypothesis choose which instances P(t₁), P(t₂), ... to use hard
∃ in the goal find a witness t with P(t) hard
proof fn some_zero(s: Seq<int>)
  requires
    s.len() > 0,
    forall|i: int| 0 <= i < s.len() ==> s[i] == 0,
  ensures
    exists|k: int| 0 <= k < s.len() && s[k] == 0,
{
  // will not verify unless this is added
  assert(s[0] == 0);
}
proof fn third_even(s: Seq<int>)
 requires
    s.len() == 10,
    forall|i: int| 0 <= i < s.len()
      ==> #[trigger] is_even(s[i]),
{
  assert(is_even(s[3])); // not verifying unless added
  assert(s[3] % 2 == 0);
}

verus can choose triggers automatically:

==> is_even(#[trigger] s[i])
requires
  s.len() == 10,
  forall|i: int| 0 <= i < s.len() - 1
    ==> #[trigger] s[i] <= s[i + 1],

with this trigger, to show s[0] <= s[3], term s[0] gives an instance for i = 0, s[1] gives an instance for i = 1, and so on.

forall |x: int| #[trigger] f(x) < f(x + 1)

in principle it does not stop. Z3 cuts chains off with heuristics, so one can get failure, timeout, or pf that breaks after unrelated edit.

trigger rules:

lemmas:

proof fn lemma_triangle_mono(i: nat, j: nat)
  requires
    i <= j,
  ensures
    triangle(i) <= triangle(j),
  decreases j,
{
  if i < j {
    lemma_triangle_mono(i, (j - 1) as nat);
  }
}

induction as recursion:

proof by induction proof fn in Verus
statement: for all n, P(n) proof fn lemma(n) ensures P(n)
base case branch with no recursive call
induction hypothesis P(n - 1) ensures of a recursive call lemma(n - 1)
inductive step code around that recursive call
induction is well-founded decreases n

Calling a lemma on a smaller input assumes it holds for smaller inputs; decreases rules out circular reasoning.

lemma with sequences:

spec fn sum(s: Seq<u32>) -> int
  decreases s.len(),
{
  if s.len() == 0 { 0 }
  else { sum(s.drop_last()) + s.last() }
}

proof fn lemma_sum_bound(s: Seq<u32>)
  requires
    forall|k: int| 0 <= k < s.len() ==> s[k] <= 1000,
  ensures
    sum(s) <= 1000 * s.len(),
  decreases s.len(),
{
  if s.len() > 0 {
    lemma_sum_bound(s.drop_last());
  }
}

fn total(v: &Vec<u32>) -> (t: u32)
  requires
    v.len() <= 1000,
    forall|k: int| 0 <= k < v.len() ==> v[k] <= 1000,
  ensures t == sum(v@),
{
  let mut t: u32 = 0;
  let mut i: usize = 0;
  while i < v.len()
    invariant
      i <= v.len() <= 1000,
      forall|k: int| 0 <= k < v.len() ==> v[k] <= 1000,
      t == sum(v@.subrange(0, i as int)),
      decreases v.len() - i,
  {
    proof {
      lemma_sum_bound(v@.subrange(0, i as int)); // (1)
      assert(v@.subrange(0, i + 1).drop_last()
        =~= v@.subrange(0, i as int)); // (2)
    }
    t = t + v[i];
    i = i + 1;
  }
  assert(v@.subrange(0, v.len() as int) =~= v@); // (3)
  t
}
  1. gives t <= 1000 * i < 2^32. without: possible overflow
  2. connects sum of first i+1 elems to sum of first i. without: invariant not satisfied at end of loop body
  3. whole prefix is whole vector. without: postcond not satisfied

verus explicit escape hatches:

Week 5. Oct 8

so far: synthesize from examples (lec 1), verify against logical specs (lec 2-4). today: synthesize from logical specs.

eg. max2: post r≥x ∧ r≥y ∧ (r=x ∨ r=y). many solutions: ite(x<=y, y, x), ite(y<=x, x, y); we need only one.

asking Z3 directly (f as uninterpreted function) fails:

f = Function('f', IntSort(), IntSort(), IntSort())
s.add(ForAll([x, y], And(f(x, y) >= x, f(x, y) >= y,
                         Or(f(x, y) == x, f(x, y) == y))))
s.check()  # unknown, timeout after 1 min

even on success, it gives a Z3 model, not a program in a language we chose.

we can restrict the syntax with a grammar:

E ::= x | y | 0 | 1 | E+E | E-E | ite(B, E, E)
B ::= E <= E | E = E | B and B | not B

size = number of rules used to build it. ite(x<=y, y, x) has size 6.

why restrict syntax?

syntax-guided synthesis (SyGuS):

eg. max2 in SyGuS input format:

(set-logic LIA)

; unknown function with grammar
(synth-fun max2 ((x Int) (y Int)) Int
  ((E Int) (B Bool))
  ((E Int (x y 0 1 (+ E E) (- E E) (ite B E E)))
   (B Bool ((<= E E) (= E E) (and B B) (not B)))))

; inputs, implicitly ∀
(declare-var x Int)
(declare-var y Int)
; spec
(constraint (>= (max2 x y) x))
(constraint (>= (max2 x y) y))
(constraint (or (= (max2 x y) x) (= (max2 x y) y)))
(check-synth)
; (define-fun max2 ((x Int) (y Int)) Int (ite (<= x y) y x))

one format, many problems:

kind unknown is example
integer arithmetic (LIA) an integer function max2
bit-vectors (BV) a bit-twiddling expression average (lec 1)
programming by example (PBE) a string transformation FlashFill (lec 1)
invariants (INV) a loop invariant loops (lec 3)

eg. BV average:

(set-logic BV)
; true average: add with one extra bit (33 bits), so nothing overflows
(define-fun spec ((a (_ BitVec 32)) (b (_ BitVec 32))) (_ BitVec 32)
  ((_ extract 31 0) (bvlshr (bvadd ((_ zero_extend 1) a) ((_ zero_extend 1) b))
                            (_ bv1 33))))
(synth-fun avg ((a (_ BitVec 32)) (b (_ BitVec 32))) (_ BitVec 32)
  ((E (_ BitVec 32)))
  ((E (_ BitVec 32) (a b (_ bv1 32)
                     (bvand E E) (bvor E E) (bvxor E E)
                     (bvadd E E) (bvsub E E) (bvlshr E E)))))
(declare-var a (_ BitVec 32))
(declare-var b (_ BitVec 32))
(constraint (= (avg a b) (spec a b)))
(check-synth)
; cvc5 (17s): (bvadd (bvand a b) (bvlshr (bvxor a b) #b0...01))

eg. FlashFill: constraints are just the examples.

(set-logic SLIA)
(synth-fun f ((s String)) String
  ((S String) (I Int))
  ((S String (s "-" (str.++ S S) (str.substr s I I)))
   (I Int (0 1 3 (str.len s) (+ I I) (- I I) (str.indexof s "-" I)))))
(constraint (= (f "(425)-706-7709") "425-706-7709"))
(constraint (= (f "(510)-220-5586") "510-220-5586"))
(check-synth)
; (str.++ (str.substr s 1 3) (str.substr s (str.indexof s "-" 0) (str.len s)))

enumerative search: list expressions from smallest to largest, check each against spec, return first that passes.

bottom-up enumeration:

bank[1] = [x, y, 0, 1]
for n = 2, 3, 4, ...:
  for each rule: # e.g. E ::= E + E
    for each way to split n-1 among children:
      combine expressions from bank
  bank[n] = everything built this round
  check each new E against spec

we can check against examples first:

eg. observational equivalence:

expression (-1,0) (0,-1) (-1,-1) (0,0)
x -1 0 -1 0
x+0 -1 0 -1 0
x-0 -1 0 -1 0
ite(x<=x, x, y) -1 0 -1 0
x+y -1 -1 -2 0

each column is an example input (x,y). first 4 rows agree on every example. they are the same expression. we can keep only smallest expression of each output signature.

where do examples come from?

counterexample-guided inductive synthesis (CEGIS):

eg. verifier is a standard verification query:

def spec(r):
    return And(r >= x, r >= y, Or(r == x, r == y))

def verify(r):  # r: candidate as Z3 term
    s = Solver()
    s.add(Not(spec(r)))  # does some input break it?
    if s.check() == unsat:
        return None      # verified
    return s.model()     # counterexample

verify(x + (y + 1))      # [y = 0, x = 0]
verify(If(x <= y, y, x)) # None

eg. CEGIS on max2

iter examples so far (x,y) learner proposes verifier says
1 none x fails on (-1,0)
2 (-1,0) y fails on (0,-1)
3 (-1,0), (0,-1) 0 fails on (-1,-1)
4 (-1,0), (0,-1), (-1,-1) x+(y+1) fails on (0,0)
5 ... and (0,0) ite(x<=y, y, x) verified

does CEGIS terminate?

however, search space grows exponentially. while smallest solution to max2 is size 6, smallest solution to max3 is size 16.

sketching: a program with hols (??). the synthesizer fills the hols so program meets spec.

eg. avg(a, b) = (a ?? b) + ((a ?? b) >> ??): first two holes are &, |, ^, +, third is a shift amount.

a, b = BitVecs('a b', 32)
op1, op2 = Ints('op1 op2')  # holes: which operator
k = BitVec('k', 32)         # hole: shift amount

def OP(h, x, y):  # 0: &  1: |  2: ^  3: +
    return If(h == 0, x & y, If(h == 1, x | y, If(h == 2, x ^ y, x + y)))

def sketch(x, y):
    return OP(op1, x, y) + LShR(OP(op2, x, y), k)

def spec(x, y):   # true average, one extra bit
    return Extract(31, 0, LShR(ZeroExt(1, x) + ZeroExt(1, y), 1))

examples = []
while True:
    learner = Solver()
    # find holes that fit examples
    learner.add(0 <= op1, op1 <= 3, 0 <= op2, op2 <= 3)
    for (x, y) in examples:
        learner.add(sketch(x, y) == spec(x, y))
    if learner.check() == unsat:
        break  # no completion of the sketch works
    h = learner.model()
    holes = [(v, h.eval(v, True)) for v in (op1, op2, k)]
    candidate = substitute(sketch(a, b), *holes)  # a, b stay free

    # look for a counterexample
    verifier = Solver()
    verifier.add(candidate != spec(a, b))
    if verifier.check() == unsat:
        break  # verified
    m = verifier.model()
    examples.append((m.eval(a, True), m.eval(b, True)))

# (a & b) + ((a & b) >> 0)   counterexample: a, b = (1840699758, 613567268)
# verified: (a & b) + ((a ^ b) >> 1)

two kinds of learner:

enumerative constraint-based (sketch)
searches all exprs in a grammar completions of one sketch
learner lists exprs by size, tests on examples one SMT query over hole vars
good at needs only a grammar from user large constants, many holes at once
struggles with large programs someone must write the sketch

eg. invariant synthesis as a SyGuS problem:

(set-logic LIA)
; x = 0; y = 0; while (x < 10) { x = x + 1; y = y + 2; }   post: y == 20
(synth-fun inv ((x Int) (y Int)) Bool)
(declare-var x Int)
(declare-var y Int)
(declare-var x1 Int)  ; x, y after one iteration
(declare-var y1 Int)
; establishment
(constraint (=> (and (= x 0) (= y 0)) (inv x y)))
; preservation
(constraint (=> (and (inv x y) (< x 10) (= x1 (+ x 1)) (= y1 (+ y 2)))
                (inv x1 y1)))
; exit
(constraint (=> (and (inv x y) (>= x 10)) (= y 20)))
(check-synth)

without a grammar, cvc5 returns the disjunction of all 11 reachable states (x=0 ∧ y=0) ∨ (x=1 ∧ y=2) ∨ ... ∨ (x=10 ∧ y=20): correct but useless. we can add grammar:

; x = 0; y = 0 while (x < 10) { x = x + 1; y = y + 2; }
(synth-fun inv ((x Int) (y Int)) Bool
  ((B Bool) (E Int))
  ((B Bool ((<= E E) (= E E) (and B B)))
   (E Int ((+ E E) x y 0 1 2 10)))
)
; (and (<= x 10) (= y (+ x x)))

; x = 0; while (x < 10) { x += 2; }
(synth-fun inv ((x Int)) Bool
  ((B Bool) (E Int))
  ((B Bool ((<= E E) (= E E) (and B B)))
   (E Int ((+ E E) (mod E 2) x 0 1 2 10)))
)
; (and (<= x 10) (= y (+ x x)))

Week 6. Oct 15

houdini: to make I, it needs a finite list built from templates, filled in with variables and constants in the program:

template our loop
v≤c, v≥c, v=c (c: 0 or a constant in the code) x≤10, x≥0, y≥0, y≤20, y=0, x=0
y = kx (k: ratio of the increments) y=2x

start big, then delete:

  1. let I be conjunction of all candidate facts (far too strong)
  2. establishment: delete every fact that is false in the initial
  3. perservation: ask Z3 for CTI: a state s that satisfies I and loop guard, but successor s' fails I
  4. if there is, delete every fact that is false at s', and go back to step 3
  5. else I is inductive. check exit: if it holds, I proves the postcond

facts are only deleted never added.

why deleting is safe:

step candidate conjuncts
start x≤10, x≥0, y≥0, y≤20, y=2x, y=0, x=0
establishment all hold at (0,0): keep all seven
preservation CTI (0,0) ⇒ (1,2): y=0 and x=0 are false at(1,2), drop
preservation no CTI: conjunction is inductive
result x≤10 ∧ x≥0 ∧ y≥0 ∧ y≤20 ∧ y=2x (and exit holds)

correct: but larger than neeed: x≥0 ∧ y≥0 ∧ y≤20 add nothing.

if we had removed 'x even constraint':

x = 0;
while (x < 10) x = x + 2;
// post: x == 10

then CTX x=9⇒11 deletes x≤10, and x≥0 alone cannot show x=10.

if the true invariant requires a ∨ b and the list does not contain it, houdini also fails.

three kinds of counterexample:

failed check solver returns tells the learner
establishment state s: P(s), but not I(s) s is reachable, I must include it (positive)
exit state s: I(s) ∧ ¬b, but not Q(s) I must exclude s (negative)
preservation pair s ⇒ s′: I(s) ∧ b, but not I(s′) if I includes s, it must include s′ (implication)

the third kind does not tell whether s is reachable.

why not just pick a label? take implication (0,-2) ⇒ (1,0); neither is reachable.

ICE learning:

                              candidate I
┌───────────────────────────┐             ┌──────────────────────┐
│          learner          │ ==========> │       verifier       │
│ find I that is consistent │             │ check establishment, │
│ with all the data so far  │ <========== │  preservation, exit  │
└───────────────────────────┘             └──────────────────────┘
                   positive / negative / implication

what learner must satisfy:

data rule for candidate I how I can satisfy it
positive p I(p) include p
negative n ¬I(n) exclude n
implication s⇒s′ I(s) ⇒ I(s′) exclude s, or include s′

picking candidate:

def learn(pos, neg, imp):
    for size in range(0, len(FACTS) + 1):
        for conj in combinations(FACTS, size):
            if all(holds(conj, p) for p in pos) and \
              not any(holds(conj, n) for n in neg) and \
              all(not holds(conj, s) or holds(conj, t) for s, t in imp):
          return conj
    return None # no conjunction fits
iter candidate I failed check new data
1 true exit negative (10,21)
2 y≤20 exit negative (10,0)
3 y=2x exit negative (11,22)
4 x=0 preservation implication (0,-2) ⇒ (1,0)
5 x≤10 ∧ y=2x none done

comparison:

Houdini ICE
direction starts with all facts, only deletes any candidate the learner likes
uses CTIs, only to delete facts positive, negative, and implication data
result largest inductive conjunction our learner: smallest consistent conjunction
on our loop five facts, three redundant x ≤ 10 ∧ y = 2x
learner fixed anything that respects the three rules

the complete picture:

spec + grammar/sketch ─► learner (search) ⇄ verifier (SMT) ─► program
                         enumeration        Z3 / cvc5
                         sketch holes       lec 2-4
                         ICE learning

to compute max3, an enumerator that goes from smallest first must get past 97b smaller expressions:

(define-fun max3 ((x Int) (y Int) (z Int)) Int
  (ite (<= z y) (ite (<= x y) y x) (ite (<= z x) x z)))

looking at answer, every branch returns x, y or z. all difficulty is in deciding which one to return. we can search two parts separately.

divide and conquer (terms and conditions):

building the decision tree:

  1. pick the condition that splits the examples most cleanly
  2. split each side again, until one term is correct on every example in the leaf
  3. read the tree back as nested ite
                    x≤z?
           yes /           \ no
            y≤z?            x≤y?
       yes /   \ no    yes /   \ no
    return z  return y  return y  return x
   (0,-1,1)   (0,1,0)   (0,1,-1)  (1,0,0)
   (0,0,1)                        (1,0,-1)
   (-1,0,1)

ite(x <= z, ite(y <= z, z, y), ite(x <= y, y, x))

bottom-up vs top-down:

size bottom-up (lec 5) top-down
1 x y 0 1 ??
3 x+y, x<=y, ... ite(??, ??, ??)
... ite(x<=y, y, x) ite(x<=y, ??, ??)
6 ite(x<=y, y, x)

eg. (deduction) we have examples [1,2,3] ⇒ [2,4,6], [5,0] ⇒ [10,0]. every program is one block applied to xs: xs, map(f, xs), filter(p, xs), reverse(xs), sort(xs). and f, p are small exprs over v, e.g. f ::= v | 0 | 1 | 2 | f + f | f * f.

reject a partial program early:

candidate what examples require verdict
xs, reverse(xs), sort(xs) no holes: just run them wrong output
filter(??p, xs) output is a sublist of input, but 4 ∉ [1,2,3] impossible for every p
map(??f, xs) output has same length as input (3 and 2) keep

rejecting filter(??p, xs) rejects every p at once, without building or running any of them. bottom-up would build and test each filter(p, xs) separately.

split the problem:

eg. (component-based synthesis), we want a small program to turn off the rightmost 1 bit of a 32-bit integer.

def slow(x):
    for i in range(32):
        if (x >> i) & 1:
            return x & ~(1 << i)
    return x

def fast(x): return x & (x - 1) # (Hacker's Delight)

a program is a list of lines:

t1 = ?? op ??
t2 = ?? op ??
...
return the last line

eg. (preferring likely programs) to write max2, smaller program is not always best:

real programs follow patterns: comparisons sit inside ite, constants are small, etc. ⇒ search programs in order of how likely they are.

rule prob
E ::= x 0.25
E ::= y 0.25
E ::= 0 0.10
E ::= 1 0.10
E ::= E + E 0.10
E ::= E - E 0.05
E ::= ite(B, E, E) 0.15
B ::= E <= E 0.60
B ::= E = E 0.20
... 0.20

Week 7. Oct 23

automatic vs. interactive proving:

Lean 4: functional programming language + proof assistant. programs, specs and proofs in one language, checked by one small kernel.

def double (n : Nat) : Nat := n + n
#eval double 21  -- 42

a theorem = name + statement + proof:

theorem double_eq (n : Nat) : double n = 2 * n := by
  unfold double  -- replace double by its body
  omega          -- finish with arithmetic

Lean accepts it only if the proof establishes exactly this statement. no error = proved.

writing statements: a statement is itself a value, of type Prop.

math Lean
for every natural number n, P(n) ∀ n : Nat, P n
P implies Q P → Q
P and Q, P or Q, not P P ∧ Q, P ∨ Q, ¬P
there is an x with P(x) ∃ x, P x
f(x, y) f x y

hypotheses are arguments:

theorem le_chain (a b c : Nat) (h1 : a ≤ b) (h2 : b ≤ c) : a ≤ c := by ...
-- same theorem
theorem le_chain : ∀ a b c : Nat, a ≤ b → b ≤ c → a ≤ c := by ...

proof state:

a b c : Nat     ┐
h1 : a ≤ b      ├ context: variables and hypotheses available
h2 : b ≤ c      ┘
⊢ a ≤ c         ← goal: what remains to be proved

eg. one tactic at a time

theorem and_swap (p q : Prop) : p ∧ q → q ∧ p := by
  intro h                -- h : p ∧ q          ⊢ q ∧ p
  obtain ⟨hp, hq⟩ := h    -- hp : p, hq : q     ⊢ q ∧ p
  constructor            -- two goals: ⊢ q (case left), ⊢ p (case right)
  · exact hq
  · exact hp

· focuses on one goal at a time.

tactic argument effect on the goal
intro h a new name P → Q: assume P as h, goal Q
exact e a proof of the goal, e.g. a hypothesis closes the goal
apply f a lemma whose conclusion matches the goal new goals: f's missing hypotheses
obtain ⟨a, b⟩ := h a hypothesis, names for its parts splits h : P ∧ Q, or h : ∃ x, P x
constructor none P ∧ Q becomes two goals
rw [h] equations replaces the left side by the right side
unfold f a definition replaces f by its body
induction n a variable one goal per case

lemmas are functions:

#check Nat.le_trans
-- Nat.le_trans {n m k : Nat} : n ≤ m → m ≤ k → n ≤ k

theorem le_chain (a b c : Nat) (h1 : a ≤ b) (h2 : b ≤ c) : a ≤ c := by
  apply Nat.le_trans h1  -- ⊢ b ≤ c
  exact h2
  -- or in one step: exact Nat.le_trans h1 h2

rw: replace equals by equals

theorem rw_demo (a b c : Nat) (h1 : a = b) (h2 : b = c) : a + 1 = c + 1 := by
  rw [h1]  -- ⊢ b + 1 = c + 1
  rw [h2]  -- ⊢ c + 1 = c + 1, closed by rfl

sorry: closes a goal without proof (placeholder). Lean warns about every one, like Verus's admit().

automation for small steps:

tactic what it does example goal
omega decides linear arithmetic over Nat and Int n + n = 2 * n
simp rewrites with a large set of simplification lemmas xs ++ [] = xs
decide evaluates a statement with no variables 2 ^ 10 = 1024
grind SMT-style search: congruence closure, linear and polynomial arithmetic a * (b + 1) = a * b + a

inductive types:

inductive Nat where
  | zero : Nat
  | succ (n : Nat) : Nat

inductive List (α : Type) where
  | nil : List α                          -- []
  | cons (x : α) (xs : List α) : List α   -- x :: xs

every value is built from finitely many constructors ⇒ functions defined by recursion, properties proved by induction

recursive definition:

def triangle : Nat → Nat
  | 0 => 0
  | n + 1 => (n + 1) + triangle n

#eval triangle 4  -- 10

lean checks termination itself (triangle (n + 1) calls triangle n), no decreases needed

induction:

theorem triangle_formula (n : Nat) : 2 * triangle n = n * (n + 1) := by
  induction n with
  | zero => rfl            -- ⊢ 2 * triangle 0 = 0 * (0 + 1): both sides evaluate to 0
  | succ k ih =>           -- ih : 2 * triangle k = k * (k + 1)
                           -- ⊢ 2 * triangle (k + 1) = (k + 1) * (k + 1 + 1)
    rw [triangle]          -- ⊢ 2 * (k + 1 + triangle k) = (k + 1) * (k + 1 + 1)
    rw [Nat.mul_add, ih]   -- ⊢ 2 * (k + 1) + k * (k + 1) = (k + 1) * (k + 1 + 1)
    grind                  -- polynomial identity, no triangle left

a proof is a term:

#print and_swap
-- theorem and_swap : ∀ (p q : Prop), p ∧ q → q ∧ p :=
--   fun p q h => And.casesOn h fun hp hq => ⟨hq, hp⟩

Mathlib: Lean's library of mathematics. much of the work is finding the right lemma: exact? searches the library (for le_chain it suggests exact Nat.le_trans h1 h2).

eg. Euclid: there are infinitely many primes

theorem infinitely_many_primes (n : ℕ) : ∃ p, n ≤ p ∧ p.Prime := by
  let p := Nat.minFac (n.factorial + 1)   -- smallest prime factor of n!+1
  have hp : p.Prime := Nat.minFac_prime
    (by have := Nat.factorial_pos n; omega)
  refine ⟨p, ?_, hp⟩                      -- witness p; goal left: n ≤ p
  by_contra h                             -- suppose p < n
  have h1 : p ∣ n.factorial :=            -- then p | n!
    Nat.dvd_factorial hp.pos (by omega)
  have h2 : p ∣ n.factorial + 1 := Nat.minFac_dvd _   -- and p | n!+1
  have h3 : p ∣ 1 := (Nat.dvd_add_right h1).mp h2     -- so p | 1
  exact hp.ne_one (Nat.dvd_one.mp h3)     -- but p is prime: contradiction

with an AI model proposing proofs and Lean checking them, this is CEGIS's propose-and-check loop with a far larger generator. Lean still can't tell whether the formal statement matches the intended claim (same as spec vs. intent in software).

functional programs are already Lean definitions:

def sum : List Nat → Nat
  | [] => 0
  | x :: xs => x + sum xs

theorem sum_append (xs ys : List Nat) : sum (xs ++ ys) = sum xs + sum ys := by
  induction xs with
  | nil => simp [sum]
  | cons x xs ih =>
    simp [sum, ih]
    omega

real code has machine integers, mutation, loops, panics ⇒ translate it into Lean definitions, prove theorems about those.

Rust to Lean with Aeneas:

           Charon            Aeneas
Rust crate ──────► LLBC ──────────► Lean code ◄── proofs (developer)
 lib.rs        Rust's internal      Code.lean      Proofs.lean
               form, cleaned up

what the translation looks like:

pub fn avg_bad(a: u32, b: u32) -> u32 {
    (a + b) / 2
}
def avg_bad (a : Std.U32) (b : Std.U32) : Result Std.U32 := do
  let i ← a + b
  i / 2#u32

inductive Result (α : Type) where
  | ok (v : α)
  | fail (e : Error)   -- e.g. integerOverflow
  | div                -- does not terminate

theorem avg_bad_overflows :
    avg_bad 4294967295#u32 1#u32 = fail .integerOverflow := by
  rfl   -- concrete input: evaluate both sides

a specification is a theorem:

-- pub fn avg(a: u32, b: u32) -> u32 { a / 2 + b / 2 + (a % 2 + b % 2) / 2 }
theorem avg_spec (a b : U32) :
    avg a b ⦃ r => r.val = (a.val + b.val) / 2 ⦄ := by
  unfold avg
  step*
#check @U32.add_spec
-- U32.add_spec : ∀ {x y : U32}, ↑x + ↑y ≤ U32.max → x + y ⦃ z => ↑z = ↑x + ↑y ⦄

mutable references become return values:

pub fn swap(a: &mut u32, b: &mut u32) {
    let t = *a;
    *a = *b;
    *b = t;
}
def swap (a : Std.U32) (b : Std.U32) : Result (Std.U32 × Std.U32) := do
  ok (b, a)

theorem swap_spec (a b : U32) : swap a b = ok (b, a) := by
  rfl

loops become recursive functions:

// 1 + 2 + ... + n
pub fn sum_to(n: u32) -> u32 {
    let mut s = 0;
    let mut i = 0;
    while i < n {
        i = i + 1;
        s = s + i;
    }
    s
}
def sum_to_loop (n s i : Std.U32) : Result Std.U32 := do
  if i < n
  then
    let i1 ← i + 1#u32
    let s1 ← s + i1
    sum_to_loop n s1 i1
  else ok s
partial_fixpoint

def sum_to (n : Std.U32) : Result Std.U32 :=
  sum_to_loop n 0#u32 0#u32

the loop lemma is the loop invariant:

theorem sum_to_loop_spec (n s i : U32)
    (hn : n.val ≤ 10000)             -- keeps s within 32 bits
    (hi : i.val ≤ n.val)             -- invariant
    (hs : s.val = triangle i.val) :  -- invariant
    sum_to_loop n s i ⦃ r => r.val = triangle n.val ⦄ := by   -- postcond
  unfold sum_to_loop
  split                              -- one goal per branch of the if
  · step*
    · -- no overflow in s + i1
      have h1 := triangle_formula i.val
      have h2 : i.val * (i.val + 1) ≤ 10000 * 10001 :=
        Nat.mul_le_mul (by omega) (by omega)
      scalar_tac
    · -- the invariant holds again
      rw [s1_post, hs, i1_post, triangle]
      omega
  · -- the loop exits: i = n
    have : i.val = n.val := by scalar_tac
    simp [hs, this]
termination_by n.val - i.val
decreasing_by scalar_decr_tac

goals left after step* are the familiar obligations (lec 3):

goal obligation meaning
hmax no overflow s + i1 fits in 32 bits
hs preservation after one iteration, invariant holds again
isFalse exit invariant and ¬(i < n) give the postcond
in sum_to_spec establishment invariant holds for s=0, i=0 (top-level theorem applies the lemma with these)

Verus vs. Lean on the same loop (both need spec, loop invariant, ranking function):

Verus Aeneas + Lean
invariant, ranking fn annotations in the Rust source hypotheses of a lemma about the loop function, termination_by
proof Z3 proves the VCs developer writes tactics, automation does the arithmetic
when stuck "postcondition not satisfied" / timeout open goal shows what is missing
trusted base Verus, Z3 Charon, Aeneas, Lean kernel