Termination Proofs
Prove that recursive functions always terminate. Lean requires termination proofs to ensure functions are total.
Why Termination Matters
lean
1-- This wouldn't be safe:2-- def bad (n : Nat) : Nat := bad n -- Infinite loop!34-- Lean rejects functions that might not terminate5-- Every recursive call must make "progress"ℹ
In a total language like Lean, every function must terminate. This ensures that proof terms always reduce to values.
Automatic Termination
lean
1-- Lean often proves termination automatically2def factorial : Nat → Nat3 | 0 => 14 | n + 1 => (n + 1) * factorial n5-- ✓ Lean sees: n < n + 167def length : List α → Nat8 | [] => 09 | _ :: xs => 1 + length xs10-- ✓ Lean sees: xs is structurally smaller1112def fib : Nat → Nat13 | 0 => 014 | 1 => 115 | n + 2 => fib (n + 1) + fib n16-- ✓ Both recursive calls decreaseStructural Recursion
lean
1-- Lean's primary termination checker uses structural recursion2-- Arguments must decrease according to a well-founded order34def sum : List Nat → Nat5 | [] => 06 | x :: xs => x + sum xs -- xs is a subterm of x :: xs78def treeSize : Tree α → Nat9 | .leaf => 010 | .node left _ right => 1 + treeSize left + treeSize right11 -- left and right are subterms of nodeKey Takeaway
Structural recursion means calling the function on subterms of the input. Lean automatically recognizes this pattern.
termination_by Clause
lean
1-- When Lean can't infer termination, specify what decreases2def gcd (a b : Nat) : Nat :=3 if b = 0 then a4 else gcd b (a % b)5termination_by b -- b decreases each call67-- For multiple arguments, use a tuple or expression8def ack : Nat → Nat → Nat9 | 0, n => n + 110 | m + 1, 0 => ack m 111 | m + 1, n + 1 => ack m (ack (m + 1) n)12termination_by m n => (m, n) -- Lexicographic orderdecreasing_by Tactic
lean
1-- When termination isn't obvious, provide a proof2def div (n m : Nat) (h : m > 0) : Nat :=3 if n < m then 04 else 1 + div (n - m) m h5termination_by n6decreasing_by7 simp_wf -- Simplify well-founded goal8 omega -- Prove n - m < n when m > 0 and n ≥ mDeep Dive: Well-Founded Recursion
Termination is based on well-founded relations. A relation is well-founded if there are no infinite descending chains.
lean
1-- Natural numbers with < are well-founded2-- No infinite chain: n > n-1 > n-2 > ... > 1 > 0 (stops!)34-- Lean uses this to ensure termination:5-- Each recursive call must use "smaller" arguments6-- where "smaller" means related by a well-founded orderCommon Patterns
Fuel Pattern
lean
1-- Add a "fuel" parameter that decreases2def search (fuel : Nat) (f : Nat → Bool) (n : Nat) : Option Nat :=3 match fuel with4 | 0 => none -- Out of fuel5 | fuel + 1 =>6 if f n then some n7 else search fuel f (n + 1)8termination_by fuelMutual Recursion
lean
1mutual2 def isEven : Nat → Bool3 | 0 => true4 | n + 1 => isOdd n56 def isOdd : Nat → Bool7 | 0 => false8 | n + 1 => isEven n9end10-- Termination: n decreases in both directionsNested Recursion
lean
1def f91 (n : Nat) : Nat :=2 if n > 100 then n - 103 else f91 (f91 (n + 11))4-- Requires sophisticated termination proof!5-- This is McCarthy's famous f91 functionpartial Escape Hatch
lean
1-- When you can't prove termination, use partial2partial def infiniteLoop : Nat → Nat3 | n => infiniteLoop (n + 1) -- No termination proof needed45-- partial functions can't be used in proofs!6-- They may not terminate, breaking soundness⚠
partial functions cannot appear in proof terms. They're only for executable code where termination isn't needed for soundness.Examples
Binary Search
lean
1def binarySearch (arr : Array Int) (target : Int) (lo hi : Nat) : Option Nat :=2 if lo ≥ hi then none3 else4 let mid := (lo + hi) / 25 if arr[mid]! < target then binarySearch arr target (mid + 1) hi6 else if arr[mid]! > target then binarySearch arr target lo mid7 else some mid8termination_by hi - lo9decreasing_by all_goals simp_wf; omegaQuick Sort
lean
1def quickSort : List Nat → List Nat2 | [] => []3 | pivot :: rest =>4 let smaller := rest.filter (· < pivot)5 let larger := rest.filter (· ≥ pivot)6 quickSort smaller ++ [pivot] ++ quickSort larger7termination_by xs => xs.length8-- smaller and larger have length < rest.length + 1