Tutorial: Interactive Proofs

This tutorial teaches you how to develop proofs interactively using the VS Code goal state viewer. Instead of writing a complete proof tactic and hoping it works, you'll build proofs step by step, observing how each tactic transforms the proof state.

The interactive workflow

The key insight: you don't need to write a complete proof all at once. Instead:

  1. State the theorem without a tactic annotation
  2. Open the goal state viewer to see what needs to be proved
  3. Add tactics one at a time, observing their effect
  4. Iterate until all subgoals are closed

Setting up

Let's work through a concrete example. Create a file with these definitions:

let rec rev = function
  | [] -> []
  | x :: xs -> rev xs @ [x]
 
let k x = x + 1 - 1
 
lemma k_eq x = k x = x [@@rw]
 
lemma rev_len x =
  List.length (List.rev x) = List.length x
[@@by auto] [@@rw]
 
lemma len_append x y =
  List.length (x @ y) = List.length x + List.length y
[@@by auto] [@@rw]
 
lemma add_commutes x y =
  x + y = y + x
[@@by auto] [@@rw] [@@perm]

Now let's prove this theorem interactively:

theorem foo x y =
  k (List.length (List.rev (x @ y))) = (List.length (y @ x))

Step 1: See the initial goal

Click on the theorem in VS Code. The goal state viewer shows:

Goal:
  k (List.length (List.rev (x @ y))) = List.length (y @ x)

No hypotheses yet — this is a universally quantified statement.

Step 2: Try simplification with specific rules

Add a tactic and observe:

theorem foo x y =
  k (List.length (List.rev (x @ y))) = (List.length (y @ x))
[@@by [%simp_only rev_len]]

After [%simp_only rev_len], the goal becomes:

Goal:
  k (List.length (x @ y)) = List.length (y @ x)

The rev has been eliminated using the rev_len rewrite rule.

Step 3: Apply more rules

Extend the tactic:

[@@by [%simp_only rev_len]
   @> [%simp_only len_append, k]]

After applying len_append and unfolding k, the goal becomes:

Goal:
  List.length x + List.length y = List.length y + List.length x

Step 4: Close with arithmetic

This is just commutativity of addition. The add_commutes permutative rule handles it, or arith can close it directly:

[@@by [%simp_only rev_len]
   @> [%simp_only len_append, k]]

In fact, [%simp rev_len, len_append] handles the entire proof in one step because it uses all available rules including add_commutes:

theorem foo x y =
  k (List.length (List.rev (x @ y))) = (List.length (y @ x))
[@@by [%simp rev_len, len_append]]

Working with subgoals

When induction or case splits generate multiple subgoals, the goal state viewer shows each one. Let's see this with a more complex example:

let rec square_sum xs =
  match xs with
  | [] -> 0
  | x :: xs -> x * x + square_sum xs
 
theorem square_sum_non_neg xs =
  square_sum xs >= 0

Attempt 1: Try auto

[@@by auto]

If auto doesn't succeed immediately, we can try a manual approach.

Attempt 2: Induction + manual

[@@by induction ()
  @>| [
    (* Base case: xs = [] *)
    [%norm square_sum xs] @> arith;
 
    (* Inductive step: xs = a :: xs' *)
    [%norm square_sum xs]
     @> [%use square_psd (List.hd xs)]
     @> arith
  ]]

Where square_psd is a helper:

lemma square_psd x = x * x >= 0

The goal state viewer shows each subgoal after induction ():

Subgoal 1 (base case):

Goal: square_sum [] >= 0

After [%norm square_sum xs]: 0 >= 0, closed by arith.

Subgoal 2 (inductive step):

Hypotheses:
  IH: square_sum (List.tl xs) >= 0

Goal: square_sum xs >= 0

After normalizing and using square_psd, the goal becomes arithmetic over non-negative values, which arith handles.

Tips for interactive proof development

Start with auto

Always try [@@by auto] first. It succeeds more often than you'd expect.

Use simp to explore

When auto fails, try [@@by simplify ()] to see how far simplification gets. The remaining goal tells you what's missing.

Extract lemmas

If a subgoal looks like it should be true but isn't closing, extract it as a separate lemma:

(* Instead of proving this inline... *)
theorem big_theorem x y z = complex_property x y z
[@@by (* long tactic that's getting unwieldy *)]
 
(* Extract the hard part as a lemma *)
lemma helper x = tricky_part x
[@@by auto] [@@rw]
 
(* Now the main theorem is easier *)
theorem big_theorem x y z = complex_property x y z
[@@by auto]

Use [%use ...] liberally

When you can see that a known fact would close the goal but the simplifier isn't applying it, use [%use ...] to add it explicitly.

Read the error messages

When a tactic fails, the error message often tells you exactly what went wrong. Common situations:

  • "No matching rewrite rule" — the rule you expected isn't firing; check the pattern
  • "Subgoal not closed" — the tactic didn't fully prove the goal; you need additional steps
  • "Induction failed" — the chosen scheme doesn't match the problem; try a different one

What you've learned

  • The interactive proof development workflow: state → observe → add tactic → iterate
  • How to use the VS Code goal state viewer to understand proof states
  • Step-by-step tactic application and observing transformations
  • When to extract lemmas from complex proofs
  • Debugging proofs when tactics don't succeed