Tutorial: Verifying Insertion Sort
In this tutorial, we'll verify that insertion sort is correct — meaning it produces sorted output that contains exactly the same elements as the input. This is a gentle introduction to induction, rewrite rules, and ImandraX's automatic proof capabilities.
The algorithm
Insertion sort works by maintaining a sorted prefix and inserting each new element into its correct position:
let rec insert (x : int) xs =
match xs with
| [] -> [x]
| a :: bs ->
if x <= a then x :: xs
else a :: (insert x bs)
let rec insert_sort xs =
match xs with
| [] -> []
| x :: xs -> insert x (insert_sort xs)Both functions are structurally recursive (recursing on a sublist), so ImandraX proves termination automatically.
Part 1: Insertion sort produces sorted output
First, let's define what "sorted" means:
let rec is_sorted (x : int list) =
match x with
| [] -> true
| [_] -> true
| x :: x' :: xs -> x <= x' && is_sorted (x' :: xs)The key lemma
To prove the main theorem, we need a helper lemma: inserting into a sorted list preserves sortedness.
theorem insert_invariant a x =
is_sorted x ==> is_sorted (insert a x)
[@@by auto] [@@rw]Notice two things:
[@@by auto]— ImandraX proves this automatically using the waterfall (with induction onx)[@@rw]— This installs the theorem as a rewrite rule. Whenever the simplifier seesis_sorted (insert a x)and knowsis_sorted x, it rewrites it totrue
The main theorem
With the rewrite rule in place, the main theorem follows:
theorem insert_sort_sorts x =
is_sorted (insert_sort x)
[@@by auto]ImandraX inducts on x:
- Base case (
x = []):insert_sort [] = [], andis_sorted []istrue✓ - Inductive step (
x = a :: xs): By the IH,is_sorted (insert_sort xs). Theninsert_sort (a :: xs) = insert a (insert_sort xs). Byinsert_invariant, sinceinsert_sort xsis sorted, so isinsert a (insert_sort xs)✓
Part 2: Insertion sort preserves elements
Sorting isn't just about ordering — we also need to verify that no elements are added or removed. We prove this by showing that the number of occurrences of each element is preserved.
let rec num_occurs x y =
match y with
| [] -> 0
| hd :: tl when hd = x ->
1 + num_occurs x tl
| _ :: tl ->
num_occurs x tlHelper lemmas
We need two lemmas about how insert affects element counts:
(* Inserting x adds exactly one occurrence of x *)
theorem lemma_1 x a =
num_occurs a (insert a x) = num_occurs a x + 1
[@@by auto] [@@rw]
(* Inserting x doesn't change the count of any other element *)
theorem lemma_2 x a b =
a <> b ==> num_occurs b (insert a x) = num_occurs b x
[@@by auto] [@@rw]Both are installed as rewrite rules so the simplifier can use them automatically.
The main theorem
theorem insert_sort_elements a (x : int list) =
num_occurs a x = num_occurs a (insert_sort x)
[@@by auto]With the rewrite rules from lemma_1 and lemma_2 in place, ImandraX proves this automatically.
The complete verified program
Here's the entire file in one piece:
(* Correctness of insertion sort in ImandraX *)
let rec insert (x : int) xs =
match xs with
| [] -> [x]
| a :: bs ->
if x <= a then x :: xs
else a :: (insert x bs)
let rec insert_sort xs =
match xs with
| [] -> []
| x :: xs -> insert x (insert_sort xs)
let rec is_sorted (x : int list) =
match x with
| [] -> true
| [_] -> true
| x :: x' :: xs -> x <= x' && is_sorted (x' :: xs)
theorem insert_invariant a x =
is_sorted x ==> is_sorted (insert a x)
[@@by auto] [@@rw]
theorem insert_sort_sorts x =
is_sorted (insert_sort x)
[@@by auto]
let rec num_occurs x y =
match y with
| [] -> 0
| hd :: tl when hd = x -> 1 + num_occurs x tl
| _ :: tl -> num_occurs x tl
theorem lemma_1 x a =
num_occurs a (insert a x) = num_occurs a x + 1
[@@by auto] [@@rw]
theorem lemma_2 x a b =
a <> b ==> num_occurs b (insert a x) = num_occurs b x
[@@by auto] [@@rw]
theorem insert_sort_elements a (x : int list) =
num_occurs a x = num_occurs a (insert_sort x)
[@@by auto]What you've learned
- Rewrite rules (
[@@rw]) are the key to modular proofs — prove small lemmas and let the simplifier compose them - Induction is applied automatically when
[@@by auto]encounters recursive functions - Two-part correctness: sorting algorithms need both a sortedness proof and an element preservation proof
- Lemma-driven development: break complex proofs into lemmas, prove each independently, and let the waterfall compose them
Next steps
The Merge Sort tutorial extends these ideas with custom termination measures and manual tactic proofs.