This whole blogpost is a chapter I submitted as part of my bachelors' thesis in Computer Science. For those interested, the full thesis can be found in the following link. Only some minor changes have been made from the original text, mainly to translate the $\LaTeX$ source from the report into the Markdown used to generate these pages.
I wrote the following text after having used Rocq for a couple of months, so I wouldn't count myself as an expert in the topic. However, I was particularly proud at the time on how I managed to explain all the concepts that I was juggling with on my head, and after recently redescovering my thesis' report I decided this would be an interesting chapter to host in my blog. I've wanted to post more things in this website for a while, but the kinds of content I want to include would require more of my time than I'm willing to give right now. In the meantime, I believe this to be a nice addition.
The Rocq Proof Assistant
From Rocq's website:
Rocq is a formal proof management system. It provides a formal language to write mathematical definitions, executable algorithms and theorems together with an environment for semi-interactive development of machine-checked proofs. Typical applications include the certification of properties of programming languages, the formalization of mathematics, and teaching.
Of the mentioned typical applications in this project we are most interested in the first, the ``certification of properties of programming languages''. In particular we are interested in verifying that certain algorithms satisfy certain properties that ensure they are implemented correctly and fulfill their purpose. The Rocq proof assistant has been used extensively to verify a multitude of projects.
CompCert
, a certified compiler for the majority of the C language.Iris
, a higher-order concurrent separation logic framework, used for reasoning about safety of concurrent programs, as the logic in logical relations, to reason about type-systems, data-abstraction, .... It has been used in other projects such as RustBelt.Fiat-Crypto
, a tool for cryptographic primitive code generation.ConCert
, a framework for smart contract verification in Rocq.Cosette
, an automated prover for checking equivalences of SQL queries.
In essence, it is a tool for writing and verifying mathematical proofs and developing certified software. To provide this functions, Rocq provides a way of creating programs, functioning as a programming languages, and a way to develop proofs, functioning as a proof assistant.
Rocq as a functional programming language
Although, at its core, Rocq is a typed functional programming language, we interact with this language in a peculiar way. When you write a program in Rocq you can think of it as having a conversation with the language, where each statement is a question and its result is what Rocq gives you as an answer. It is not that different from writing your programs in a REPL in other languages. However, Rocq allows you to be very creative with your questions. For example, the following command will prompt Rocq to show the internal representation of its grammar. Pay attention to the final dot. All Rocq commands must end in a dot.
Print Grammar constr.
Entry constr is
[ LEFTA
[ "@"; global; univ_annot
| term LEVEL "8" ] ]
and lconstr is
...
At first this might come off as odd. Why would one need such a command? The answer is that, while in most programming languages we regard the grammar as something static, Rocq allows you to alter the way it parses your definitions. Therefore, it is useful to know what Rocq currently thinks the grammar looks like.
However, these commands are not to what we refer to when we talk about Rocq being a programming language.
The programming language inside Rocq can be accessed through some special commands, such as
Inductive
or Definition
.
Inductive
allows us to define data types. They work similarly to enumerations in other
languages. Consider, for example, the following definition of the booleans as a type containing two
constructors, false
.
Inductive bool :=
| true
| false.
An inductive type defined in this way can have any number of constructors, including zero!
Inductive void := .
You can check for the type of a value with the Check
command.
Check false.
false
: bool
While the Definition
allows us to define values. This works in a similar way to general programming languages. The
following statement assigns to the variable named my_true
the value
true
.
Definition my_true := true.
A function can be defined through the Definition
command by specifying its arguments on
the left side of the :=
symbol. The parenthesis around the arguments are only needed if you
want to specify the argument's type, or if the type itself could not be inferred by Rocq itself. For
example we define the function always_true
which takes a boolean argument
true
.
Definition always_true (b: bool) := true.
To express the return type of a function we also use the :type
syntax, putting it
right before the :=
symbol. The following line defines the identity function for
values of type bool
.
Definition bool_identity (b: bool) : bool := b.
To the right side of the :=
symbol we can put any Rocq expression. Technically, the
language of expressions in Rocq is called Gallina
% https://coq.inria.fr/doc/v8.9/refman/language/gallina-specification-language.html#:~:text=This%20chapter%20describes%20Gallina%2C%20the,%2C%20functions%2C%20predicates%20and%20sets.
but people usually refer to it as just Rocq informally. Calling a function is an expression, and the syntax
used is the same as when defining one. To illustrate this we introduce the Compute
command, which prompts Rocq to evaluate an expression and show its result.
Compute bool_identity true.
= true
: bool
A useful construct of Gallina is that of the
match _ with ... end
which lets us act differently depending on the way values were
constructed. This can be used, for example, to define the boolean notb
function,
which exchanges false
and vice versa.
Definition notb b := match b with
| true => false
| false => true
end.
Observe that in this case we could omit the parenthesis in the argument definition, since Rocq was
able to infer the type of b
from how it was used in the expression.
We can also create functions which return anonymous functions by using the fun
keyword. The arguments are specified with the same syntax as before, with the main difference being
that now the =>
symbol is used to separate the arguments from the body, and not
->
symbol to separate the
argument type from the return type. Consider the following function which implements a curried
version of the boolean and function.
Definition andb (b: bool) : bool -> bool :=
fun b' => match b with
| true => b'
| false => false
end.
As with most functional programming languages, functions in Rocq can be curried, which means
that they can be partially applied, providing only one argument and returning a function which takes
the others. Therefore this type of definition is, in fact, unnecessary. We could have achieved the
same result by having the function take both arguments b'
directly.
When defining a new type with Inductive
, the constructors themselves can also take
arguments. This works as the main way of storing values in Rocq, similar to how records work in other
programming languages. For example, this is how we would define a named pair of two boolean values.
Inductive two_bools := my_bools (b b': bool).
Notice that b'
is a valid identifier in Rocq. While this is not unique to Rocq (OCaml
also has this feature), it is still novel enough to be worth pointing it out. Also notice that since
both b'
had the same type we could specify it under the
same set of parenthesis.
Inductive
definitions are also allowed to be recursive, as long as Rocq is able to
prove that this recursion comes to an end. In general this means that at least one constructor is not
recursive. For example, here is the definition of Peano natural numbers in Rocq.
Inductive nat :=
| O
| S (n: nat).
You can also define recursive functions, but not through the Definition
command. We
use the match
over a type
with arguments in its constructors, it can be useful to use an identifier instead of a value in order
to bind the value to the identifier. Observe how this is used in the following definition of addition
for Peano's natural numbers.
Fixpoint add (n m: nat) :=
match n with
| O => m
| S n' => add n' (S m)
end.
If the argument itself will not be used, you can also use the special
identifier _
to discard it, like it is used in the following example
of the function is_zero
.
Fixpoint is_zero(n: nat): bool :=
match n with
| O => true
| _ => false
end.
Fixpoint
functions in Rocq also have the catch that Rocq must be able to ensure that
they are terminating. Therefore, a function that loops infinitely cannot be defined in Rocq.
Fixpoint loop (n: nat): nat := loop n.
Recursive definition of loop is ill-formed.
In environment
loop : nat -> nat
n : nat
Recursive call to loop has principal argument equal to
"n" instead of a subterm of "n".
Recursive definition is: "fun n : nat => loop n".
These are the basic building blocks of Rocq's programming language. There are not any built-in data
types commonly found in other languages like booleans or numbers. These are instead defined as part of
the standard library, in Coq.Init.Nat
. Common
expressions such as the if
expression are actually just syntactic sugar over the
let ... in
expression,
which lets you bind an expression to a variable and use it in another expression.
let x := 10 in x + 1
In Rocq, this statement desugars to a match
expression.
match 10 with
| x => x + 1
end
To let Rocq know that we want to use the definitions for bool
and
nat
defined in its standard library, we would use the following commands.
From Coq Require Import Bool.Bool.
From Coq Require Import Init.Nat.
Besides including the definitions mentioned before, the standard library also defines some common functions, such as addition, and provide some syntactic sugar to work with them. For example, the two following statements are equivalent since they make use of the same function.
Compute add 1 2.
= 3
: nat
Compute 1 + 2.
= 3
: nat
To show the syntactic sugar that Rocq is currently aware of you can use the following command.
Print Visibility.
...
Visible in scope nat_scope
"x >= y" := (ge x y)
"x > y" := (gt x y)
"x <= y <= z" := (and (le x y) (le y z))
"x <= y < z" := (and (le x y) (lt y z))
"n <= m" := (le n m)
"x < y <= z" := (and (lt x y) (le y z))
"x < y < z" := (and (lt x y) (lt y z))
"x < y" := (lt x y)
"x - y" := (Nat.sub x y)
"x + y" := (Nat.add x y)
"x * y" := (Nat.mul x y)
One thing to point out from the output of this command is the fact that boolean equality of two numbers,
recognized as the function ==
or
=?
symbol. This is to highlight that this operator returns a boolean
value, since unlike other programming languages, booleans are not built-in into the language. Furthermore,
the =
operator is reserved for some other purpose in Rocq, which we will explore
in the next section.
Rocq also allows overloading a symbol to different operations depending on the types of its
operands. This can ease the readability of some proofs and theorems, however it can also
sometimes lead to ambiguity. To prevent this, Rocq offers the possibility to define this
syntactic sugar under a scope, and later specify under which scope a given piece of syntax
should be interpreted by. This is done by enclosing the syntax in parenthesis and following
it with the %
symbol and the name of the scope, which usually tends to
correspond to a type.
This is useful, for example, when juggling with multiple
definitions of numbers. Besides nat
, Rocq also defines the data-types
Z
for naturals and integers. The differences
between N
lie in how they are defined.
nat
implements the Peano natural numbers, encoded as zero and its
successors. N
on the other hand follows the computer representation
of numbers, using a sequence of zeroes and ones. Fortunately, we can use the +
symbol to express addition for both data-types.
Check (1 + 1)%nat.
(1 + 1)%N
: N
Check (1 + 1)%N.
(1 + 1)%nat
: nat
Rocq as a theorem prover
Besides the commands that allow you to interface with the programming language, Rocq also offers commands
with which you can define theorems and prove them. In fact, the commands Theorem
,
Example
, the main way in which you interact with the
theorem proving side of Rocq, are all equivalent with one another. The different names are simply for the
developers to classify the properties they are trying to prove as they see fit.
Consider the following proposition. Pay attention to how we now use the :
symbol to separate the
proposition with its name instead of the :=
symbol as we did in the programming language.
Example true_is_true : true = true.
The proposition appears after the first :
symbol, and it follows a similar syntax
to expressions. % Warning 1 of Cury-Howard correspondance
After this command is executed Rocq registers the proposition you want to prove and awaits for you
to provide a proof of this proposition. We do this by entering proof mode using the
Proof
command.
In proof mode, Rocq shows you the proposition you have to prove as a goal. In graphical interfaces this is usually the proposition which appears under the horizontal line.
1 subgoal
========================= (1 / 1)
true = true
The space above the horizontal line is reserved for local definitions, which are definitions that can only be accessed from within the proof. Most of the time these definitions refer to our hypothesis. There are no hypothesis at the moment, which is why the only text above the horizontal line is the goal counter. The goal is the current proposition we need to prove. There can be more than one goal to prove in a proof, and we say that the proof has concluded when there are no goals left.
Inside proof mode, a new set of commands a syntax are available to us to construct the proof. These are
called tactics. For example, to prove the goal true = true
we wouldd use the
reflexivity
tactic, which concludes an equality if both sides are syntactically the same. This is
exactly the case for this goal, which has true
on both sides.
Once the goal has been proven you can exit proof mode using the Qed
command. Therefore,
a theorem and its proof would look like this
Example true_is_true : true = true.
Proof.
reflexivity.
Qed.
This example is not terribly exciting. It would be more interesting if we could prove a property
for a greater amount of values, not just one. To achieve this in Rocq we use the
forall
keyword, which has a similar syntax to that of function application.
Example bool_refl' : forall b : bool, b = b.
If we enter proof mode, the goal is now forall b : bool, b = b
. We cannot conclude this
proof with reflexivity
, since the goal is not an equality, it has a quantifier
beforehand. What we would like to say is that the proof is the same regardless of our choice of
b
and
proving the theorem for that intros
tactic.
Proof.
intros b.
reflexivity.
Qed.
Proving something for all elements of a type is pretty powerful, but usually we are only interested in
proving things for elements which satisfy certain properties. To do this we make use of implication,
written with the ->
symbol.
Example bool_eq_sym' : forall b b' : bool, b = b' -> b' = b.
To start the proof we introduce the variables b'
with
intros
as before. This leaves us in the following state.
1 subgoal
b, b' : bool
========================= (1 / 1)
b = b' -> b' = b
To proceed, we use eq_b_b'
to reflect its meaning
1 subgoal
b, b' : bool
eq_b_b' : b = b'
========================= (1 / 1)
b' = b
What we have introduced this time under the name eq_b_b'
is not a value, but the
evidence that the proposition eq_b_b'
is true. We can later use that evidence with
other tactics, like rewrite
, which takes an equality and rewrites all appearances of
the expression on the left side of the =
symbol with that on the right side of the
equality.
1 subgoal
b, b' : bool
eq_b_b' : b = b'
========================= (1 / 1)
b' = b'
At this point we can conclude the proof with the reflexivity
tactic.
Example bool_eq_sym' : forall b b' : bool, b = b' -> b' = b.
Proof.
intros b b'.
intros eq_b_b'.
rewrite eq_b_b'.
reflexivity.
Qed.
The dual of the universal quantifier forall
is the existential quantifier
exists
, which states that there is a value of one type which satisfies the specified
proposition.
Example has_zero : exists n: nat, n = 0.
When proving a goal with an exists
, Rocq expects you to provide a value with the
equally-named exists
tactic, and show that the property holds for the given value.
Proof.
(* Goal is [exists n : nat, n = 0.] *)
exists 0.
(* Goal is [0 = 0.] *)
reflexivity.
Qed.
In these proofs so far, we have treated all elements of a type the same way, not making a distinction on how they were constructed. However, for some proofs it is necessary to make this distinction.
Theorem negb_involutive : forall b : bool, negb (negb b) = b.
To prove this statement, we need to show that it holds true for both true
and
destruct
tactic, which takes a term as
an argument and creates as many subgoals as constructors it has. In each of these subgoals, the term
has been replaced by one of its constructors. Therefore, destruct
allows us to prove
that a proposition is true depending on how the term was constructed.
In this case, the first subgoal asks us to prove negb (negb true) = true
.
We can use the simpl
tactic to ask Rocq to simplify the goal, for example by executing
the negb
function through its definition. This leaves us with
reflexivity
tactic. The
same argument holds for the second subgoal
Proof.
intros b. destruct b.
- simpl.
reflexivity.
- simpl.
reflexivity.
Qed.
As a matter of fact, the true = true
goal is something we already proved in the
true_is_true
example before. We could reuse this proof
by using the apply
tactic instead, which receives the name of a proposition previously
defined and attempts to fit it into the current goal, by trying to infer the values of quantified
variables. For example, we could have also used the
bool_eq_refl'
theorem, and Rocq would have set the value of
true
.
The apply
tactic also works if the proposition to apply is part of an implication.
If we have a lemma Q
, we can use
P
, since if we are able to
prove h
. An example
of such a situation would be using the previously defined
bool_eq_sym'
.
Lemma h': forall b: bool, b = true -> true = b.
Proof.
intros b b_true.
(* Goal is [true = b] *)
apply bool_eq_sym'.
(* Goal is [b = true] *)
apply b_true.
Qed.
Notice how we conclude this example by applying a proof which lives as a local definition. The apply tactic is not restricted to the theorems available to Rocq, but also to those arising from the context of the proof.
Negation in Rocq is not a first-class feature, but it is implemented in terms of implication and
False
proposition represents a proposition which cannot be
proven, its dual is the True
proposition, for which there is one trivial proof called
I
.
Then, the negation of a proposition ~ P
, is defined as
P
allows
you to derive a proof of P
in the first place, since there is no proof for P
.
The same way we have reflexivity
to end a proof if the goal is an equality and both
sides of the equality are constructed with the same constructors, the discriminate
tactic allows us to end a proof if given one hypothesis of an equality which can be shown to be false
since both sides use different constructors.
For instance, consider the following proposition.
Example one_not_zero : ~ (1 = 0).
Proof.
unfold not. (* Transforms [~ (1 = 0)] into [(1 = 0) -> False] *)
intros h. (* Introduces hypothesis [h: 1 = 0] *)
discriminate h. (* Concludes that hypothesis h cannot be obtained *)
Qed.
We have defined P -> False
. If we revert the terms in that implication we have
P
. This fact is known in the literature
as the principle of explosion or ex falso quodlibet. In Rocq this principle shows in some proofs
where instead of proving the goal it may be convenient that with the assumed hypothesis we can derive
a proof for false. This principle is accessed through the exfalso
tactic.
Theorem not_true_is_false' : forall b : bool,
~ (b = true) → b = false.
Proof.
intros b not_b_true.
destruct b.
- (* b = true *)
unfold not in not_b_true.
exfalso.
apply not_b_true.
reflexivity.
- (* b = false *)
reflexivity.
Qed.
Besides negating a proposition, Rocq also allows us to use the usual logical connectives. And is expressed
with the symbol \/
, mimicking their shape's
in mathematical notation: $\wedge$ and $\vee$.
When dealing with split
tactic to
divide the goal into two, which we will have to prove separately. If the /\
is instead
in a hypothesis destruct h
tactic, which would
separate both sides of the conjunction in different hypothesis. Rocq will try to give appropriate names
to the new hypothesis, but most of the time it is a better idea to give the names on your own. To do so,
you can use hr
are the names given to the left and right sides of the conjunction respectively.
When dealing with \/
in the goal, we need to decide which part of the proposition we would
like to prove in order to prove the hypothesis. To choose the left side of the disjunction we use the
appropriately named right
tactic choosing the
right side of the disjunction as expected. If the \/
is instead in a hypothesis
destruct h
tactic, which will ask us to
prove the same goal twice: once with the left side of the disjunction h
and once with
the right side of the disjunction h
. In both of these options the sides of the
disjunction would replace the h
hypothesis, but it is also possible to rename them
by employing the tactic as hl
and
hr
are again the names of the left and right side of the disjunction respectively.
Before moving on to the next topic, I wanted to point out a particularity of Rocq's mathematical foundations. Notice that when handling a goal of type $P \vee Q$ we need to know which between $P$ or $Q$ holds before proceeding with the proof. This is due to Rocq's logic being constructive. In a non-constructive logic proving that a proposition cannot be false is equivalent to proving that that proposition is true. In a constructive logic, however, a proposition is only true if you show it to be true. While nuanced, this is a powerful distinction.
Consider the following proposition:
There exist irrational numbers $a$ and $b$ such that $a^b$ is rational.
In classical logic, a proof of the previous would proceed like this. We assume we have already proven that $\sqrt{2}$ is irrational. Then consider whether $\sqrt{2}^{\sqrt{2}}$ is rational. If it is, we can conclude the proof with $a = b = \sqrt{2}$. Otherwise $\sqrt{2}^{\sqrt{2}}$ is irrational. We choose $a = \sqrt{2}^ {\sqrt{2}}$ and $b = \sqrt{2}$. Then $$ a^b = \left(\sqrt{2}^{\sqrt{2}}\right)^{\sqrt{2}} = \sqrt{2}^{\sqrt{2}\sqrt{2}} = \sqrt{2} ^ 2 = 2 $$ Since $2$ is rational this concludes the proof.
Notice how we have concluded the proof without knowing the values of $a$ and $b$. This happened because we were able to assume the following:
$\sqrt{2}^{\sqrt{2}}$ is rational ${\Large{\vee}} \sqrt{2}^{\sqrt{2}}$ is irrational.
A constructive logic has the advantage that, if something is proven within it, you are
guaranteed to have values for what was proven. Nevertheless, sometimes proofs require of the
excluded_middle
as an Axiom in the module Coq.Logic.ClassicalFacts
. An axiom is a proposition
which Rocq accepts as true without proof, and can be declared with the Axiom
command. However, it is dangerous to do so, one may introduce a proposition which would enable
proving true = false
. Fortunately, we can rely on Rocq's provided assumptions to
not break the consistency of its logic.
The Curry-Howard correspondence
In the field of programming languages and type theory, the Curry-Howard correspondence states that there
exists an isomorphism between programs and proofs, and types and propositions. So far we have treated Rocq
the programming language and Rocq the proof assistant as two different things, but in fact, they are the
same. Whenever we wrote Theorem
, we could have used
Definition
instead!
Definition true_is_true' : true = true.
Proof.
reflexivity.
Qed.
The type of true_is_true
is in fact the proposition we are trying to prove, and its
value is the proof built in proof mode. In fact, not even proof mode was necessary. We could have
specified the ``proof object'' directly after the :=
symbol. This is made apparent if we
ask Rocq to print one of our prior proofs.
Print has_zero.
has_zero =
ex_intro (fun n : nat => n = 0) 0 eq_refl
: exists n : nat, n = 0
Here we see that we construct a proof value using the ex_intro
constructor, with the
following parameters:
n = 0
- 0 is the witness of the
n
for which the property is true reflexivity
tactic!
The feature which allows us to use Rocq as a theorem prover is its type system. In fact, Rocq's type system
is not like those of other general purpose language, since it allows the definition of dependent types. A
dependent type is a type whose definition depends on a value. This allows us to make types such as
Vector.t nat 10
, of all lists of natural numbers of length 10,
or more importantly, since propositions are types it allows us to refer to individual values in our
propositions, instead of just their types.
Dependent data types can be created with the Record
command.
Record dependent_example := {
a: nat;
proof_a_not_0: a <> 0
}.
The dependent_example
type represents all pairs of naturals
a
and
proofs that a
is not 0. We can generalize this example to define the subset of
elements of a type $A$ which fulfill some condition $P$, that is ${x \in A : P (x)}$. This type of record is defined in Rocq's standard
library and it is called sig
.
Print sig.
Inductive sig (A : Type) (P : A -> Prop) : Type :=
exist : forall x : A, P x -> {x : A | P x}.
Arguments sig [A]%type_scope P%type_scope
Arguments exist [A]%type_scope P%function_scope x _
sig
is usually used through a special syntax also defined in the standard library
where sig A P
.
The forall
keyword behaves like function application, with the difference that it
allows the return type to use the argument's value, that is, it allows the return type to depend on
the function's parameters. Otherwise, forall n : nat, nat
is the same as
nat -> nat
, as can be seen in the following example.
Definition a : forall n: nat, nat := add 1.
Check a.
a
: nat -> nat
The False
is equivalent to our previous definition of
void
, an inductive type with no constructors, and
I
. Intuitively it
makes sense, since there is not proof of falsehood, therefore it has no constructors, and you can always
prove True
without needing any extra information, hence the constructor without
arguments.
Dependent types also allow us to achieve something similar to parametric polymorphism. Consider the
previously defined bool_identity function
. This function is a less powerful version
of the generic identity
function which works for arguments of any type. These types
of functions are defined in other functional programming languages through the use of parametric
polymorphism, making both the input and output types generic. However, since in Rocq types are first
class values we do not need parametric polymorphism to define such a function.
Definition identity' (A: Type)(b: A) := b.
Compute identity' bool true.
This definition is possible since we are allowed to make the type of the second argument depend on the value of the first with dependent types. However, using this identity function is noticeably less ergonomic than its counterpart in other functional programming languages since we also need to explicitly specify the type of its argument. In other functional programming languages, this task is left to the type checker through the feature of type inference. Instead, Rocq introduces a more powerful feature: implicit arguments. An explicit argument is defined by surrounding it with curly brackets instead of parenthesis.
Definition identity {A: Type}(b: A) := b.
Compute identity true.
Rocq will attempt to derive the value for implicit arguments automatically, using globally available
information as well as its signature. You can also ask Rocq to infer the value of a parameter which was
not marked as implicit by using the _
identifier instead of its value.
Compute identity' _ true.
To provide all arguments of a function explicitly, even if they were defined as implicit, you can
refer to the function with a prepended @
.
Compute @identity bool true.
This is occasionally useful if Rocq is struggling to infer the implicit argument (Rocq's algorithm for type checking and type synthesis can be exponential in time in the worst case).
In other programming languages, many commonly used functions are made available in the form of a
standard library. Rocq does the same thing, with its standard library perhaps containing more basic
definitions than in most other functional programming languages since the booleans and integers are
delegated to it. For example, Rocq's standard library defines the list
data type and
its constructors, and the usual operations over them such as map
,
fold_right
or
existsb
. These are defined in the
Coq.Lists.List
module.
Print existsb.
existsb =
fun (A : Type) (f : A -> bool) =>
fix existsb (l : list A) : bool :=
match l with
| nil => false
| a :: l0 => f a || existsb l0
end
: forall A : Type, (A -> bool) -> list A -> bool
However, besides providing data types and operations Rocq also provides theorems which let us reason
about these data types and operations. For example, in Coq.Lists.List
the
existsb_exists
lemma is defined, which establishes the relationship between the
exists
quantifier.
Print existsb.
existsb_exists
: forall (A : Type) (f : A -> bool) (l : list A),
existsb f l = true <-> (exists x : A, In x l /\ f x = true)
These lemmas are used extensively throughout this project, and they are also the reason why we try to define things in terms of functions in the standard library whenever possible.