The following are my notes for Software Foundations Volume 1: Logical Foundations.
NOTE: It is not a comprehensive summary of the book. Some fundamental parts (like Polymorphism, Maps, and Imp) are not included while optional parts are. I highly recommend doing the exercises of the book.
Contents
- Inductive Types
- Functions
- Proof Basics
- Reusing Lemmas
- Notation Declarations
- The Search Command
- Type Inference and Implicit Arguments
- Higher Order Functions
- Currying and Uncurrying
- Destructing Compound Expressions
- Constructive Logic
- Inductively Defined Propositions
- Automation
- Tactics Reference
- Exercises for me To Do
Inductive Types
The number of built-in features in Rocq is very small. As a useful first approximation, types in Rocq can be thought of as sets. For example, the type day is defined as follows. In this case, the type is well-defined and finite.
Inductive day : Type :=
| monday
| tuesday
| wednesday
| thursday
| friday
| saturday
| sunday.
Rocq also supports types with infinite size, such as the natural numbers, which are defined with two constructors: one for 0 and one for the successor of an object of type nat. Natural numbers are therefore represented in unary format, which differs from the decimal format humans use or the binary format computers use. The unary representation is chosen because it makes proofs simpler. The recursive structure of numbers directly mirrors how we structure proofs by induction. I find it interesting that even fundamental types like numbers are not a built-in feature but are defined in the standard library. The S constructor takes a parameter of type nat, but constructors can take parameters of any type, not necessarily the same type being defined.
Inductive nat : Type :=
| O
| S (n : nat).
Functions
Once we have defined inductive types, we write functions that operate on them using Definition and pattern matching with match expressions. A match expression examines the structure of a value and executes different code based on which constructor was used.
Example: the pred (predecessor) function on natural numbers:
Definition pred (n : nat) : nat :=
match n with
| O => O
| S n' => n'
end.
This function says: if n is O (zero), return O. If n has the form S n' (the successor of some number n'), return n' directly. This works by “peeling off” one S constructor from the unary representation.
However, this has a design flaw: there is no true predecessor of zero, but we’re forced to return something. A better approach uses the Option type, which represents whether a value exists or not (similar to Option in Rust or Optional in Python).
Option is polymorphic: it takes a type parameter A, so option nat, option bool, and option anything all use the same definition. Polymorphism is a very important feature in Rocq. It lets us write generic functions and types that work with any type, avoiding code duplication and enabling powerful abstractions.
Inductive option (A : Type) : Type :=
| None
| Some (a : A).
Definition pred' (n : nat) : option nat :=
match n with
| O => None nat
| S n' => Some nat n'
end.
Now pred' 0 returns None (no predecessor exists), while pred' (S n') returns Some n' (the predecessor is n'). This is more semantically correct, we explicitly distinguish the case where no valid predecessor exists. Pattern matching must be exhaustive: we must handle all constructors of the type.
When we don’t need to use the value bound by a pattern, we use the wildcard _ to avoid inventing an unused variable name. For example:
Definition is_zero (n : nat) : bool :=
match n with
| O => true
| S _ => false
end.
Simultaneous pattern matching allows us to match multiple values at once. This is useful when comparing two values, like checking equality of two natural numbers. We use the Fixpoint keyword to write recursive functions, and Rocq only accepts recursive definitions it can check will terminate (typically by structural recursion). In this case, Rocq can see that eqb is structurally recursive because the recursive call is on smaller arguments.
Fixpoint eqb (n m : nat) : bool :=
match n, m with
| O, O => true
| O, S _ => false
| S _, O => false
| S n', S m' => eqb n' m'
end.
Function Types
Functions in Rocq also have types and can therefore be thought of as part of a set. The function andb (logical AND) has type bool -> bool -> bool. It takes two arguments of type bool and returns a single bool. It is of the same type as the function orb (logical OR). They are in the same set of function types.
The arrow -> represents a function type. A -> B means “a function that takes an argument of type A and returns a result of type B”. (Function application is written by f x.) For example, bool -> bool is a function that takes a bool and returns a bool.
When we have multiple arrows, we read them right-to-left:
bool -> boolmeans: takes onebool, returns oneboolbool -> bool -> boolmeans: takes onebool, returns another function of typebool -> bool(which itself takes abooland returns abool)
Example: the function andb that performs logical AND:
Definition andb (b1 : bool) (b2 : bool) : bool :=
match b1 with
| true => b2
| false => false
end.
This function has type bool -> bool -> bool. If the first argument b1 is true, we return b2 (the result depends on the second argument). If b1 is false, the result is false regardless of b2.
Proof Basics
Rocq is fundamentally about proofs. We define statements (propositions) about our types and then construct evidence that they hold. Proofs are introduced using the Example, Lemma, or Theorem keywords. These keywords are essentially equivalent from a technical standpoint. They all produce proven statements, but serve different naming conventions: Example for concrete test cases, Lemma for helper results used in larger proofs, and Theorem for main results.
Each proof has a goal (what we want to prove) and a context (facts and hypotheses we can use). We can reuse previously proven lemmas and theorems in our own proofs. Rocq accepts any proven statement without requiring us to reprove it.
Before diving into proofs, let’s first understand how proofs work by testing a simple concrete example:
Example eqb_refl_3 : eqb 3 3 = true.
Proof. reflexivity. Qed.
What’s happening here?
This is a simple proof that demonstrates the reflexivity principle. We’re claiming that when we compare the number 3 with itself using the eqb function, the result is true. The proof is trivial:
Exampleintroduces a concrete test case (as opposed toTheoremwhich states a general property)- The goal is to prove
eqb 3 3 = true reflexivity.works because after Rocq evaluateseqb 3 3, it computes totrue- Both sides of the equality become identical (
true = true), whichreflexivitycan solve directly Qed.confirms the proof is complete
This works for a specific number, but what if we want to prove it for any natural number? That’s where we need induction.
induction is one of the most important tactics in Rocq. Like destruct, it performs case analysis on inductively defined types by creating a subgoal for each constructor. However, induction is more powerful: for recursive constructors, it adds an induction hypothesis to the context. This hypothesis states that the property we’re proving already holds for smaller values, allowing us to prove properties about infinite sets.
We use destruct for simple case analysis on a specific value, but use induction when you need to prove a property that holds for all values of a recursive type.
To prove a statement, we use tactics that manipulate the goal and context to move us closer to a proof. Here’s a detailed example of a general proof using induction:
(* Theorem: For any natural number n, comparing n with itself using eqb returns true.
This demonstrates proof by mathematical induction. *)
Theorem eqb_refl : forall (n : nat), eqb n n = true.
Proof.
(* Step 1: Introduce the universally quantified variable n into the context.
After this, we have: n : nat ⊢ eqb n n = true *)
intros n.
(* Step 2: Apply induction principle to n.
This splits the proof into two cases:
- Base case: n = O (zero)
- Inductive case: n = S n' (successor), with induction hypothesis IHn' *)
induction n as [|n' IHn'].
(* CASE 1: Base Case (n = 0)
Goal: eqb 0 0 = true
Strategy: Simplify the goal by unfolding the definition of eqb.
Since both arguments are O, the pattern match returns true,
reducing the goal to: true = true
Then reflexivity closes the goal trivially. *)
- simpl. (* eqb 0 0 unfolds to true = true *)
reflexivity. (* true = true is proven by reflexivity *)
(* CASE 2: Inductive Case (n = S n')
Context: n' : nat
Context: IHn' : eqb n' n' = true (induction hypothesis)
Goal: eqb (S n') (S n') = true
Strategy:
1. Simplify unfolds the definition of eqb. Since both arguments are S n',
the pattern match proceeds recursively and reduces to: eqb n' n' = true
2. Rewrite using the induction hypothesis IHn' which states eqb n' n' = true.
This transforms the goal to: true = true
3. Reflexivity closes the remaining trivial goal.
This completes the inductive step: if the property holds for n',
it holds for S n' (the successor of n'). *)
- simpl. (* eqb (S n') (S n') unfolds to eqb n' n' = true *)
rewrite IHn'. (* Replace eqb n' n' = true with true using IHn' *)
reflexivity. (* true = true is proven by reflexivity *)
Qed.
(* Conclusion:
By mathematical induction:
- We proved the base case: eqb 0 0 = true
- We proved the inductive step: if eqb n' n' = true then eqb (S n') (S n') = true
- Therefore, eqb n n = true for all natural numbers n. *)
The as clause specifies how to name the variables introduced in each case. This becomes especially important when we have more inductive cases and makes names explicit.
The pattern induction n as [| n' IHn'] has one “slot” per constructor of the type you are inducting on. For nat, there are two constructors (O and S), so there are two slots:
[ ]is the base case (O), which takes no arguments.n'is the argument toSin the inductive case.IHn'is the induction hypothesis for the recursive argumentn'.
If you induct on a type with three constructors, you get three slots: induction x as [ ... | ... | ... ]. Each slot names the constructor arguments (and any induction hypotheses for recursive arguments).
One subtle point: when you introduce variables matters, because it changes the strength of your induction hypothesis.
Rule of thumb: if your goal looks like forall n m, P n m and you want to do induction on n, introduce n first, do induction n, and only then introduce m. That keeps m general in the IH.
(* weaker IH: m is fixed *)
intros n m. induction n as [|n IH].
(* stronger IH: IH works for any m *)
intros n. induction n as [|n IH]; intros m.
If you already introduced m “too early”, you can often recover by doing generalize dependent m. before induction.
Reusing Lemmas
One of the most powerful aspects of formal verification is the ability to build complex proofs by composing simpler, previously-proven results. Instead of reproving things, we reuse lemmas and theorems.
Consider this example. First, we prove a simple helper lemma:
Lemma app_nil_r : forall l : natlist,
l ++ [] = l.
Proof.
intros l.
induction l as [|n l' IHl'].
- reflexivity. (* Base case: [] ++ [] = [] by reflexivity *)
- simpl. (* Simplify: (n :: l') ++ [] becomes n :: (l' ++ []) *)
rewrite -> IHl'. (* Apply induction hypothesis: l' ++ [] = l' *)
reflexivity. (* Now we have n :: l' = n :: l', proven *)
Qed.
Now we can use this lemma in a more complex proof. When we have rewrite -> app_nil_r, Rocq searches for a pattern matching the left side of app_nil_r (which is l ++ []) in our goal and replaces it with the right side (l):
Theorem rev_length : forall l : natlist,
length (rev l) = length l.
Proof.
intros l.
induction l as [|n l' IHl'].
- reflexivity. (* Base: length (rev []) = length [] *)
- simpl.
rewrite -> app_length. (* Standard library lemma *)
rewrite -> IHl'. (* Apply induction hypothesis *)
rewrite plus_comm. (* length l' + 1 = 1 + length l' *)
simpl. reflexivity.
Qed.
Once a lemma is proven with Qed, we can use it by name in rewrite, apply, or other tactics. Rocq maintains a library of proven results, allowing us to build increasingly complex proofs without reimplementing foundational facts.
When we use Qed to complete a proof, Rocq seals the proof. In particular, the resulting constant is typically opaque: other proofs can use the lemma statement, but the kernel will not unfold/reduce the proof term during computation. When you write rewrite -> app_nil_r, Rocq only cares about the statement l ++ [] = l, not how it was proven. We can change the proof script without affecting code that depends on it, as long as the statement stays the same.
Notation Declarations
Rocq allows us to define custom notation to make expressions more readable. The Notation command lets us create infix operators or change how terms are displayed.
For example, in the Lists chapter, we define custom notation for list operations:
Notation "x :: l" := (cons x l) (at level 60, right associativity).
Notation "[ ]" := nil.
Notation "[ x ; .. ; y ]" := (cons x .. (cons y nil) ..).
These declarations mean:
x :: lis shorthand forcons x l(prepend element to list)[]is shorthand fornil(empty list)[1; 2; 3]is shorthand forcons 1 (cons 2 (cons 3 nil))(list literal syntax)
The at level and associativity specifications control how Rocq parses and pretty-prints expressions. We can use these notations immediately after declaration:
Definition my_list := [1; 2; 3].
Definition prepend := 0 :: my_list.
Notation is purely syntactic sugar and just makes code more readable and closer to mathematical convention.
The Search Command
When working on proofs, we often need previously proven theorems but can’t remember their names. The Search command helps us find relevant theorems in the library.
Basic usage:
Search rev. (* Find all theorems mentioning rev *)
Search (_ + _ = _). (* Find all theorems about addition equalities *)
Wildcards (_) match any expression. To use a variable pattern instead:
Search (?x + ?y = ?y + ?x). (* Find commutativity lemmas for + *)
You can restrict search to a specific module:
Search (_ + _) inside Induction. (* Only search theorems from Induction module *)
Many IDEs (like VSRocq) provide graphical search interfaces that make this even more convenient.
Type Inference and Implicit Arguments
When we write polymorphic functions, Rocq can often infer type arguments automatically. Making arguments implicit avoids verbose type annotations.
In our earlier option with explicit type parameters, we had to write None nat and Some nat n'. A standard way to avoid that is to keep the datatype the same, but mark constructor arguments as implicit:
Inductive option (A : Type) : Type :=
| None
| Some (a : A).
Arguments None {A}.
Arguments Some {A} _.
Definition pred' (n : nat) : option nat :=
match n with
| O => None
| S n' => Some n'
end.
Higher Order Functions
Like many other programming languages, functions in Rocq can be passed as arguments to other functions, returned as a result, stored in a data structure, combined to create new functions, or used to build abstractions that work with any data type.
The most popular Higher Order functions filter, map and reducefold are just a couple of lines in Rocq. In filter, the first explicit argument is a function which takes a variable of type X as input and returns a bool. In fold, the argument f is a function that takes an element of type X and an accumulator of type Y and returns a value of type Y.
Fixpoint filter {X : Type} (test : X -> bool) (l : list X) : list X :=
match l with
| [] => []
| h :: t => if test h then h :: (filter test t) else filter test t
end.
Fixpoint map {X Y : Type} (f : X -> Y) (l : list X) : list Y :=
match l with
| [] => []
| h :: t => (f h) :: (map f t)
end.
Fixpoint fold {X Y: Type} (f : X -> Y -> Y) (l : list X) (b : Y) : Y :=
match l with
| nil => b
| h :: t => f h (fold f t b)
end.
Often we need functions just once, for a single task. Rather than define them at the top level and give them names, Rocq lets us create functions “on the fly” using the fun keyword. An anonymous function like fun n => n * 2 is a function that takes n and returns n * 2, without needing a formal definition or name.
Here, filter selects even numbers using an anonymous predicate fun n => evenb n, then fold sums them with another anonymous function fun n acc => n + acc.
Fixpoint evenb (n : nat) : bool :=
match n with
| O => true
| S O => false
| S (S n') => evenb n'
end.
Definition sum_evens (l : list nat) : nat :=
fold (fun n acc => n + acc) (filter (fun n => evenb n) l) 0.
Currying and Uncurrying
Currying is the process of converting a function that takes a pair (product type) into a function that takes arguments one at a time. For example, a function f : (X × Y) → Z becomes f' : X → Y → Z. The curried version takes one argument, returns a function waiting for the second argument, and finally returns Z. This enables partial application. You can only pass the first argument and get back a specialized function.
Uncurrying is the reverse: converting X → Y → Z into (X × Y) → Z, where both arguments must be supplied together as a pair.
In Rocq, most functions are naturally curried (they take one argument at a time), which is why partial application works so well.
Destructing Compound Expressions
destruct can analyze not just variables, but the results of arbitrary expressions. Especially useful for boolean tests in if-then-else statements. When destructing a compound expression like (n =? 3), always use eqn: to preserve the equation, otherwise you lose critical information needed for the proof:
Definition is_three (n : nat) : bool :=
if n =? 3 then true else false.
Theorem three_only : forall n, is_three n = true -> n = 3.
Proof.
intros n H.
unfold is_three in H. (* H : (if n =? 3 then true else false) = true *)
destruct (n =? 3) eqn:E. (* CASE : (n =? 3) = true, H : true = true *)
- apply Nat.eqb_eq in E. (* E : n = 3 *)
exact E.
- discriminate H. (* CASE : (n =? 3) = false, H : false = true *)
Qed.
Without eqn:E, the equation n =? 3 = true would be discarded, making it impossible to prove n = 3.
Constructive Logic
Rocq uses constructive logic. In constructive logic, proofs must be explicit and constructive: To prove that something exists, you must actually build or exhibit it, not just argue abstractly. This requirement fits the philosophy of machine-checked proofs. Only concrete, checkable evidence can be verified by a machine. As a result, several classical logic principles are not valid in Rocq’s constructive logic:
Law of Excluded Middle
The principle that every proposition is either true or false cannot be proven for arbitrary propositions. This is not derivable in Rocq because to prove P \/ ~ P, we’d need to use left (proving P) or right (proving ~ P), but we have no information about which side holds for an arbitrary proposition P.
Excluded middle is valid for decidable propositions where we can compute a boolean answer. For example, equality on natural numbers is decidable: forall (n m : nat), n = m \/ n <> m.
Definition excluded_middle := forall P : Prop,
P \/ ~ P.
Double Negation Elimination
We cannot eliminate double negations for arbitrary propositions. We can prove P -> ~ ~ P (introducing double negation), but we cannot go from ~ ~ P back to P as this would require excluded middle.
Definition double_negation_elimination := forall P : Prop,
~ ~ P -> P.
Rocq is based on Intuitionistic type theory, a form of constructive logic whose foundation is the Curry-Howard correspondence. This principle equates propositions with types and proofs with programs: To prove a proposition is to construct a term of a certain type. Every proof is a program, and every program is a proof. This essential connection makes machine checking possible. Since proofs are executable objects, a computer can verify their correctness by type-checking and evaluation.
This is the fundamental core of Rocq: Its type checker is not just a tool for catching programming errors, but the very engine that guarantees the correctness of both programs and proofs.
The type A -> B is literally the type of functions from A to B. When A and B are propositions, a proof of A -> B is a function that transforms proofs of A into proofs of B. Consider this concrete example where a proof term is simultaneously a logical proof and an executable program:
(* A proof of implication IS a function *)
Definition modus_ponens (P Q : Prop) : (P -> Q) -> P -> Q :=
fun (f : P -> Q) (p : P) => f p.
(* This is simultaneously:
- A proof that ((P -> Q) -> P -> Q) is provable
- A program that applies a function f to an argument p *)
This correspondence extends to all logical operations.
Propositions as Types
In Rocq, propositions have a special type called Prop. All well-formed propositions have type Prop, whether they are true, false, or unprovable. Propositions are first-class citizens. They can be named with Definition, parameterized (functions returning Prop), and manipulated like any other value in Rocq.
Check (forall n m : nat, n + m = m + n) : Prop.
Check 2 = 2 : Prop.
Check 3 = 2 : Prop. (* false, but still has type Prop *)
(* Functions can return propositions *)
Fixpoint double (n : nat) : nat :=
match n with
| O => O
| S n' => S (S (double n'))
end.
Definition is_even (n : nat) : Prop := exists k, n = double k.
Check is_even : nat -> Prop.
Being a proposition is different from being provable. A proposition can exist and be well-typed without having a proof.
Proof Irrelevance and Curry-Howard
The Curry-Howard correspondence tells us that “proofs are programs,” yet Rocq also embraces proof irrelevance: the principle that all proofs of the same proposition are indistinguishable.
Rocq distinguishes between Prop (the universe of propositions) and Type (the universe of computational types). Propositions live in Prop, which is designed for logical reasoning rather than computation. Proofs in Prop are treated as computationally irrelevant (for example, they are erased during extraction). We also assume proof irrelevance as an additional principle: that any two proofs of the same proposition are equal. When we prove a theorem, we mainly care that it is provable, not how it was proven (some mathematicians may find that offensive). The proof strategy (induction, case analysis, rewriting) becomes an implementation detail once sealed with Qed.
The computational content lives in other types like nat, bool, and list, where different constructions are distinct. When we extract executable programs from Rocq, all Prop terms are erased since they carry no runtime data. This gives us both logical rigor through precise proofs and computational efficiency by discarding proof details that don’t affect program behavior.
(* Example: Prop vs Type distinction *)
Fixpoint even_comp (n : nat) : bool :=
match n with
| O => true
| S O => false
| S (S n') => even_comp n'
end.
Definition even (n : nat) : Prop :=
exists k, n = double k.
Logical Connectives in Rocq
In Rocq, logical connectives are defined as inductive types. Proofs involving them are constructed using tactics that manipulate these types.
Conjunction
Conjunction (logical AND) of propositions A and B is written A /\ B, representing the claim that both are true. The infix notation /\ is syntactic sugar for and A B, where and : Prop -> Prop -> Prop. Under Curry-Howard, A /\ B corresponds to a product type (pair). A proof of A /\ B is literally a pair (proof_of_A, proof_of_B).
To prove a conjunction, use the split tactic to generate two subgoals, one for each part. To use a conjunctive hypothesis H : A /\ B, use destruct H as [HA HB] to split it into two separate hypotheses:
Lemma and_example : forall n m : nat, n = 0 /\ m = 0 -> n + m = 0.
Proof.
intros n m [Hn Hm]. (* destruct the conjunction into two hypotheses *)
rewrite Hn. rewrite Hm.
reflexivity.
Qed.
Disjunction
Disjunction (logical OR) of propositions A and B is written A \/ B, true when either A or B holds. This stands for or A B, where or : Prop -> Prop -> Prop. Under Curry-Howard, A \/ B corresponds to a sum type (tagged union). A proof is either left proof_of_A or right proof_of_B.
To prove a disjunction, use left to prove the left side or right to prove the right side. To use a disjunctive hypothesis, perform case analysis with destruct or an intro pattern:
Lemma factor_is_O: forall n m : nat, n = 0 \/ m = 0 -> n * m = 0.
Proof.
intros n m [Hn | Hm].
- (* Case: n = 0 *)
rewrite Hn. reflexivity.
- (* Case: m = 0 *)
rewrite Hm. rewrite <- mult_n_O. reflexivity.
Qed.
Falsehood and Negation
Negation ~ P is defined as P -> False, where False : Prop is a contradictory proposition with no proof. Since False cannot be proven, assuming it allows us to prove anything via the principle of explosion.
Inequality x <> y is notation for ~ (x = y), i.e., (x = y) -> False.
Theorem zero_not_one : 0 <> 1.
Proof.
unfold not.
intros contra.
discriminate contra. (* 0 = 1 contradicts constructor disjointness *)
Qed.
Universal Quantification
Universal quantification forall x : T, P x asserts that property P holds for every value x of type T. This is Rocq’s way of expressing “for all”. The type forall x : T, P x : Prop states that for any choice of x, the proposition P x holds. forall x : T, P x is a dependent function type, where the result type P x depends on the input value x. This is more general than simple function types like T -> U where the result type is always U.
To prove a universally quantified goal, use intros x to introduce an arbitrary value x into the context, then prove P x for this generic x. To use a universally quantified hypothesis H : forall x, P x, apply it to a specific value using apply H or specialize it with specialize H with (x := value).
Theorem forall_example : forall n m : nat,
n + m = m + n.
Proof.
intros n m. (* Introduce arbitrary n and m *)
(* Now prove n + m = m + n for these specific values *)
apply add_comm.
Qed.
Logical Equivalence (If and Only If)
The iff connective P <-> Q asserts that two propositions have the same truth value. It’s defined as the conjunction of two implications: (P -> Q) /\ (Q -> P). You can often use a proven <-> to replace one side with the other.
Theorem iff_sym : forall P Q : Prop, (P <-> Q) -> (Q <-> P).
Proof.
intros P Q [HAB HBA].
split.
- (* -> *) apply HBA.
- (* <- *) apply HAB.
Qed.
Existential Quantification
Existential quantification exists x : T, P x asserts that there is at least one value x of type T for which property P holds. This is Rocq’s way of expressing “there exists.” Under Curry-Howard, exists x : T, P x corresponds to a dependent pair type (also called Sigma type). A proof is a pair (witness, proof_that_property_holds) where the second component’s type depends on the first component’s value.
Constructive requirement: In constructive logic, proving existence requires providing a witness. An actual value that satisfies the property. You cannot prove exists x, P x by arguing abstractly. You must exhibit a specific x and prove P x holds for it. This is the essence of constructive existence proofs.
To prove an existential goal, use exists value. to provide a concrete witness, then prove P value holds for that specific value. To use an existential hypothesis H : exists x, P x, destruct it with destruct H as [x Hx] to extract the witness x and the proof Hx : P x.
Theorem exists_example : forall n : nat,
(exists m, n = 4 + m) -> (exists o, n = 2 + o).
Proof.
intros n [m Hm]. (* Destruct existential: get witness m and proof Hm *)
exists (2 + m). (* Provide witness for goal *)
apply Hm.
Qed.
Inductively Defined Propositions
Inductively Defined Propositions (IDP) is another very powerful feature of Rocq. Instead of expressing propositions using universal, existential, and logical connectives, propositions are defined inductively by a set of rules.
This makes perfect sense when considering that Rocq uses constructive logic. Proving a proposition is to construct evidence. The constructors of an IDP provide the specific rules for constructing this evidence. In fact, using these constructors is the only way to prove an IDP holds.
The ev Property
A classic example is the property of a number being even. We can define ev inductively:
Inductive ev : nat -> Prop :=
| ev_0 : ev 0
| ev_SS (n : nat) (H : ev n) : ev (S (S n)).
This definition says there are exactly two ways to prove a number is even:
- Rule
ev_0: 0 is even. - Rule
ev_SS: Ifnis even, thenS (S n)(i.e., n+2) is even.
Equivalence with Other Definitions
We can define the same proposition classically and inductively, prove their equivalence and when the proposition is part of the context of a new proof, interchange them. This can make proving new things a lot easier.
We previously defined evenness using a boolean function (evenb) or the existential proposition (exists k, n = 2 * k). We can prove that our inductive definition is equivalent to these.
We want to prove ev n <-> even n. The direction even n -> ev n is straightforward (induction on k). However, proving ev n -> even n requires a new technique.
Induction on Evidence
To prove forall n, ev n -> even n, standard induction on n is difficult. The inductive step would involve n and S n, but ev jumps by two (n to S (S n)). The induction hypothesis for n doesn’t help with S n generally.
Instead, we perform induction on the evidence E : ev n. Since E is built from constructors ev_0 and ev_SS, induction on E gives us cases corresponding exactly to these rules.
Theorem ev_even_iff : forall n,
ev n <-> even n.
Proof.
intros n. split.
- (* -> *)
unfold even.
intros E.
(* We proceed by induction on the evidence E that ev n holds. *)
induction E as [|n' E' IH].
+ (* Case E = ev_0: n is 0. *)
exists 0.
reflexivity.
+ (* Case E = ev_SS n' E': n is S (S n').
The IH tells us that n' is even (exists k, n' = double k). *)
destruct IH as [k Hk].
subst.
exists (S k).
simpl.
reflexivity.
- (* <- *)
unfold even.
intros [k Hk].
rewrite Hk.
(* We use the helper lemma ev_double (prove by induction on k) *)
apply ev_double.
Qed.
In the -> direction, we can see that Rocq produced an IH that corresponds to E', the single recursive occurrence of ev in its own definition. Since E' mentions n', the induction hypothesis talks about n', as opposed to n or some other number.
Tactics for Evidence
When we have a hypothesis H that is an IDP, we can use different tactics to analyze it:
destruct H: Performs simple case analysis. It breaks the proof into cases based on which constructor was used to buildH. It does not provide an induction hypothesis.inversion H: Analyzes howHmust have been constructed. It is smarter thandestructbecause it derives necessary equations between arguments (e.g., ifev (S (S n))holds,inversioninfersev nmust hold).induction H: Likedestruct, but for recursive constructors (likeev_SS), it provides an Induction Hypothesis stating that the property holds for the smaller evidence. This is crucial for proofs likeev_even_iffabove.
With these tools, we can freely interchange between inductive (ev), boolean (evenb), and existential (even) definitions depending on which is most convenient for the current proof context. We can trivially prove the equivalence of ev and evenb using transitivity since we already know even <-> evenb and now ev <-> even.
Automation
Rocq’s auto tactic is proof search. It tries to solve the current goal by repeatedly applying hypotheses or lemmas from the context and from a hint database, together with simple steps like intros. It either solves the goal completely or does nothing.
If auto fails because it did not search deep enough, you can increase the search depth with auto n (default depth is 5), and you can inspect what it’s doing with debug auto (trace) or info_auto which shows which facts it used when it succeeds.
Hint databases are what make auto scale. Rocq ships with a default database called core, and you can add common lemmas, constructors, unfolding hints to a database:
(* Add objects to core. *)
Hint Resolve h : core.
Hint Constructors p : core.
Hint Unfold d : core.
(* Use your own database instead of polluting core. *)
Create HintDb db.
Hint Resolve h : db.
(* Use it when you need it. *)
auto with db.
eapply and eauto are variants that allow existential variables (placeholders like ?x) to be left for later instantiation. We use this when an intermediate value becomes obvious only after you solve a later subgoal.
Tactics Reference
apply · assert · assumption · auto · clear · constructor · contradiction · destruct · discriminate · eapply · eassumption · eauto · exists · exfalso · f_equal · generalize dependent · induction · injection · inversion · intros · left / right · lia · reflexivity · remember · rename · repeat · replace · rewrite · simpl · specialize · split · subst · symmetry · transitivity · try · unfold
apply
Uses a theorem or hypothesis to move between premises and conclusions.
assert
Introduces a “local lemma” into the context by stating a proposition and then proving it as a new subgoal. After the new subgoal is proved, the proposition becomes a hypothesis you can use. Usage: assert (H : P). creates a subgoal for P, then continues with H : P in the context.
assumption
Closes the goal if it exactly matches a hypothesis in the context.
auto
Attempts to solve the goal using proof search (roughly: combinations of intros and apply) using facts from the context and from hint databases (like core). It either solves the goal or does nothing. Increase depth with auto n (default 5). Use debug auto to see where it gets stuck, and info_auto to see what it used.
clear
clear H removes hypothesis H from the context.
constructor
If the goal is an inductive proposition, constructor tries to apply an appropriate constructor, leaving its premises as subgoals.
contradiction
Closes the goal if the context contains an explicit contradiction (e.g., both P and ~P, or a hypothesis equivalent to False).
destruct
Performs case analysis on an inductively defined value by creating a subgoal for each constructor. Use destruct x as [| n'] to name the variables in each case, and destruct x eqn:E to preserve the equation showing which constructor was matched.
discriminate
Proves the current goal immediately when the context contains a contradiction: an equality between values built from different constructors (e.g., true = false). Useful for eliminating impossible cases in proofs. This embodies: “From a false proposition, anything follows” (Principle of explosion), which states that from a contradictory hypothesis, any conclusion follows.
eapply
Like apply, but allows Rocq to leave some arguments as existential variables (placeholders like ?x) to be determined later. Useful when an intermediate term is hard to guess upfront, but becomes determined after solving subsequent subgoals.
eassumption
Like assumption, but also allows instantiating existential variables in the goal to match a hypothesis.
eauto
Like auto, but uses eapply during proof search, so it can solve goals that require deferring instantiation of some arguments. Typically slower than auto.
exists
When the goal is an existential exists x, P x, exists v provides the witness v and changes the goal to P v.
exfalso
Changes the current goal to False, useful when you want to prove something by deriving a contradiction.
f_equal
Transforms a goal of the form f x = f y into x = y, exploiting the property that function application preserves equality. Similar to injection, but works on goals rather than hypotheses. It reasoning backwards from an equality of constructed values to equalities of their arguments. Automatically discharged subgoals that are trivial (e.g., provable by reflexivity).
generalize dependent
Moves a variable from the context back into the goal as a universally quantified variable. Useful for strengthening induction hypotheses or reconsidering proof strategy when variables were introduced too early.
Usage: generalize dependent x. converts x : T in the context to forall x : T in the goal, allowing subsequent tactics to see x as a universally quantified variable.
induction
Proves goals involving recursive structures by breaking them into a base case (for the simplest constructor) and an inductive case (for recursive constructors), where you can assume the statement holds for smaller values via the induction hypothesis.
injection
Exploits the injectivity property of constructors: If two values built from the same constructor are equal, then their arguments must be equal. Given a hypothesis H : f x = f y where f is a constructor, injection H as Heq generates the equation x = y and adds it to the context. This works on hypotheses containing equalities, not on goals.
intros
Introduces universally quantified variables and hypotheses from the goal into the context, making them available for the proof.
inversion
inversion H is like destruct H, but smarter. Use it on a hypothesis that was built by an inductive definition.
It does two things:
- It splits into cases for each constructor that could have produced
H. - It adds the equalities that must be true for that constructor to fit the shape of
H, and it discards impossible cases.
In practice it is common to write inversion H; subst. so the generated equalities are immediately substituted.
left and right
When the goal is a disjunction P \/ Q, use left to change the goal to P, or right to change the goal to Q.
lia
Solves (or refutes) goals in linear integer arithmetic (equalities/inequalities with +, -, and logical connectives). Great for routine arithmetic side-conditions.
reflexivity
Proves equalities where both sides are identical or can be shown to be identical through computation and unfolding.
remember
remember e as x eqn:Hx replaces an expression e with a fresh name x and records the defining equation Hx : x = e. Useful before induction when e is not a variable.
rename
rename x into y renames a variable/hypothesis in the context (helpful when inversion/destruct picks awkward names).
repeat
Repeatedly applies a tactic until it fails (or stops making progress). Beware: repeat can loop if the tactic always succeeds and keeps making progress.
replace
Replaces a subexpression in the goal with another expression, creating two subgoals: one with the replacement applied, and one proving the two expressions are equal. Useful when rewrite cannot target the correct occurrence or when you want to guide the proof explicitly.
Usage: replace e1 with e2. produces subgoal 1 (goal with e1 replaced by e2) and subgoal 2 (proof that e1 = e2).
rewrite
Replaces occurrences in the goal using an equality hypothesis or theorem, allowing us to transform one expression into another. Use rewrite -> H to apply left-to-right or rewrite <- H to apply right-to-left. rewrite can be used on the goal or in the context with the in keyword (e.g., rewrite H in hyp).
- On the goal:
apply His usually backward reasoning. IfH : A -> Band your goal isB, thenapply H.changes the goal toA. - On a hypothesis:
apply H in hypis forward reasoning. IfH : A -> Bandhyp : A, thenapply H in hyp.replaceshyp : Awithhyp : B.
simpl
Simplifies the goal by unfolding definitions and performing basic computation, reducing complex expressions to simpler forms.
specialize
Instantiates a universally quantified hypothesis in the context by providing concrete arguments. If H : ∀ x : T, P is a generic hypothesis, then specialize H with (x := e) replaces H with a specialized version where x is replaced by e. This is useful for narrowing down overly general assumptions to a specific case you care about. Can be combined with the as clause to name the resulting hypothesis: specialize H with (x := e) as H'.
split
When the goal is a conjunction P /\ Q, split generates two goals: P and Q.
subst
subst x replaces x everywhere using a hypothesis like x = e (or e = x) and clears that hypothesis; subst does this for all such equalities.
symmetry
Switches the left and right sides of an equality in the goal, converting ⊢ t = u into ⊢ u = t. Useful when apply fails because the equality is the wrong direction. Can be applied to hypotheses with symmetry in H.
transitivity
Proves a goal x = z by introducing an intermediate value y and splitting into two subgoals: x = y and y = z. Usage: transitivity y. where y is the intermediate value.
try
try T runs tactic T; if T fails, it succeeds doing nothing. Useful in scripts like induction ...; try (simpl; ...) to handle “most cases.”
unfold
Manually unfolds the definition of a name introduced by Definition so you can manipulate the underlying expression. This is useful when tactics like simpl are too conservative and don’t automatically unfold because they can’t simplify further.
Exercises for me To Do
Logic
- Exercise: 5 stars, standard, optional (classical_axioms)
IndProp
- Exercise: 3 stars, standard, optional (ev_plus_plus)
- Exercise: 4 stars, standard, optional (leb_le)
- Exercise: 4 stars, advanced (subsequence)
- Exercise: 4 stars, standard, optional (exp_match_ex2)
- Exercise: 4 stars, standard, optional (weak_pumping_star_app)
- Exercise: 5 stars, advanced, optional (pumping)
- Exercise: 4 stars, advanced (filter_challenge)
- Exercise: 5 stars, advanced, optional (filter_challenge_2)
- Exercise: 4 stars, standard, optional (palindromes)
- Exercise: 5 stars, standard, optional (palindrome_converse)
- Exercise: 4 stars, advanced, optional (NoDup)
- Exercise: 3 stars, standard, optional (app_ne)
- Exercise: 3 stars, standard, optional (star_ne)
- Exercise: 2 stars, standard, optional (match_eps)
- Exercise: 3 stars, standard, optional (match_eps_refl)
- Exercise: 3 stars, standard, optional (derive)
- Exercise: 4 stars, standard, optional (derive_corr)
- Exercise: 2 stars, standard, optional (regex_match)
- Exercise: 3 stars, standard, optional (regex_match_correct)
Imp
- Exercise: 4 stars, standard, optional (optimize)
- Exercise: 3 stars, standard (stack_compiler)
- Exercise: 3 stars, standard (execute_app)
- Exercise: 3 stars, standard (stack_compiler_correct)
- Exercise: 3 stars, standard (stack_compiler_correct)
- Exercise: 3 stars, standard, optional (short_circuit)
- Exercise: 4 stars, standard, optional (break_imp)
- Exercise: 3 stars, advanced, optional (while_break_true)
- Exercise: 4 stars, advanced, optional (ceval_deterministic)
- Exercise: 4 stars, standard, optional (add_for_loop)
IndPrinciples
- Exercise: 4 stars, standard, optional (t_tree)
Rel
- Exercise: 3 stars, standard, optional (rtc_rsc_coincide)