MPRI PRFA — Proof Assistants
🏠 Go back to the course homepage.
Meta-programming
The subject this time is meta-programming in Rocq. Meta-programming is the activity of writing programs that manipulate and produce programs.
You have actually been using meta-programming all this time, because tactics are a form of meta-program if you see proof terms as programs.
In Rocq itself, there are many options to meta-program. The most basic is called Ltac and it's the tactic language you have been writing your proofs in. We will also see MetaRocq which allows you to write meta-programs in the same language as Rocq.
Other options include Ltac2, Mtac, and Rocq-ELPI.
Set Default Goal Selector "!".Ltac
Semicolon ; is a bit more complicated than just sequential composition.
First, t1 ; t2 applies t2 to all subgoals generated by t1. So split ; constructor will solve the goal nat * nat.
QUESTION: Do you know what it will produce as a witness of nat * nat?
Second, Ltac bakes in backtracking: each tactic can have several successes. For instance the constructor tactic has a success for each constructor that matches the goal.
boolconstructor.bool
It picks true
exists b : bool, b = falseexists b : bool, b = false
unshelve eexists lets us give the witness first
bool?b = falseconstructor.booltrue = false
We're stuck now
Abort.true = falseexists b : bool, b = falseunshelve eexists ; constructor.exists b : bool, b = false
This time it picked false!
Actually, it first picked true, but then constructor failed to prove true = false. So it went back and picked another success for the goal bool, namely false.
This might also be a surprise but constructor can be used to prove x = x, just like reflexivity. We'll see more about that next week.
Abort.exists b : bool, b = falseexists b : bool, b = false
constructor doesn't work for exists because it can't guess the witness, but we can use econstructor instead to generate an existential variable to be filled later. When doing this, we can prove the goal with just a succession of econstructor.
As before I use unshelve to make sure it asks us to also give the witness.
unshelve econstructor ; econstructor.
Tacticals
We will now give you a list of useufl taticals, that is tactics that take tactics as arguments.
Applying different tactics to different subgoals
forall n : nat, mult4 n -> not_mult4 n -> Falseforall n : nat, mult4 n -> not_mult4 n -> Falsen: nat
h: mult4 n
hn: not_mult4 nFalseh: mult4 1Falseh: mult4 2Falseh: mult4 3Falsen: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> FalseFalseinversion h.h: mult4 1Falseinversion h.h: mult4 2Falseinversion h.h: mult4 3Falsen: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> FalseFalsen: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> False
n0: nat
H0: mult4 n
H: n0 = nFalseauto. Abort.n: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> False
H0: mult4 nFalseforall n : nat, mult4 n -> not_mult4 n -> Falseforall n : nat, mult4 n -> not_mult4 n -> False
We are not doing the same in all branches so we can't just use ;
But we can do the following.
induction hn ; [ inversion h | inversion h | inversion h | inversion h ; subst ; auto ]. Abort.n: nat
h: mult4 n
hn: not_mult4 nFalseforall n : nat, mult4 n -> not_mult4 n -> Falseforall n : nat, mult4 n -> not_mult4 n -> False
In fact we can even factorise the three first cases
induction hn ; [ inversion h .. | inversion h ; subst ; auto ].n: nat
h: mult4 n
hn: not_mult4 nFalse
The elipsis represented by .. can be used at most once, and repeats the preceding tactic.
Abort.forall n : nat, mult4 n -> not_mult4 n -> Falseforall n : nat, mult4 n -> not_mult4 n -> Falsen: nat
h: mult4 n
hn: not_mult4 nFalse
We can put it in the middle for instance
h: mult4 2Falseh: mult4 3False
Here I used idtac which is the tactic that does nothing so the two middle goals are still unsolved.
Abort.forall n : nat, mult4 n -> not_mult4 n -> Falseforall n : nat, mult4 n -> not_mult4 n -> False
In fact idtac can even be left out
n: nat
h: mult4 n
hn: not_mult4 nFalseAbort.h: mult4 2Falseh: mult4 3Falsen: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> FalseFalseforall n : nat, mult4 n -> not_mult4 n -> Falseforall n : nat, mult4 n -> not_mult4 n -> False
We can also use try to perform a tactic only if it succeeds
n: nat
h: mult4 n
hn: not_mult4 nFalsen: nat
h: mult4 n
hn: not_mult4 nFalse
It does nothing because it failed.
h: mult4 1Falseh: mult4 2Falseh: mult4 3Falsen: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> FalseFalse
We can combine it with solve to make sure we only use inversion if it solves the goal.
Abort.n: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> FalseFalse
You can also repeat a certain tactic a number of times using do or repeat. Note that repeat may repeat the tactic 0 times without failing. So if t fails, repeat t will still succeed.
mult4 40mult4 40mult4 36mult4 32mult4 28mult4 24
No way we're doing this forever
apply mult4_0. Abort.mult4 0mult4 40mult4 40mult4 8Abort.mult4 0mult4 40repeat constructor.mult4 40
There is more of course, but we'll let you discover them on your own. Typically when you will need them. For this you can of course ask questions, but you can also look at the tactic index: https://rocq-prover.org/doc/V9.0.0/refman/coq-tacindex.html
Case analysis on the goal
In Ltac you also have access to the goal and you can examine its shape or that of the hypotheses, which can be very useful to build your own custom tactics.
nat * bool * unitnat * bool * unitA':= = (nat * bool)%type: Set
B':= = unit: Setnat * bool * unit
Note we use question marks to talk about unification variables. This is slightly different from Rocq's usual pattern matching. Also remark that we are matching on a type here!
match goal backtracks when the tactic inside fails
match goal with | |- ?A * ?B => fail
The ideal tactic to fail
A':= = (nat * bool)%type: Set
B':= = unit: Set
A'':= = nat: Set
nat * bool * unit
lazymatch goal doesn't backtrack in the branches
Fail lazymatch goal with | |- ?A * ?B => fail "This is expected this time"
We give a custom error
A':= = (nat * bool)%type: Set
B':= = unit: Set
A'':= = nat: Set
nat * bool * unit
I recommend this, unless you really want backtracking!
It also fails when none of the branches match
A':= = (nat * bool)%type: Set
B':= = unit: Set
A'':= = nat: Setnat * bool * unit
We can use this to fill automatically the goal
repeat lazymatch goal with | |- ?A * ?B => split | |- nat => exact 13 | |- bool => exact false | |- unit => exact tt end.
The repeat match basically implements some variant of auto.
We can also define tactics at top level so we can use them many times.
Ltac split_or_apply :=
match goal with
| |- ?A /\ ?B => split
| h : ?B -> ?A |- ?A => apply h Here we match on any hypothesis
The name h is of our choosing, it's going to apply to hypothesis H : nat -> bool for instance.
forall A B : Prop, (A -> B) -> A /\ Bforall A B : Prop, (A -> B) -> A /\ BA, B: Prop
H: A -> BA /\ B
No need to specify the names we won't be using them.
A, B: Prop
H: A -> BAA, B: Prop
H: A -> BBA, B: Prop
H: A -> BAadmit.A, B: Prop
H: A -> BAA, B: Prop
H: A -> BBAbort.A, B: Prop
H: A -> BAforall A B : Prop, (A -> B) -> A /\ Bforall A B : Prop, (A -> B) -> A /\ BA, B: Prop
H: A -> BA /\ BA, B: Prop
H: A -> Bid (A /\ B)A, B: Prop
H: A -> Bid (A /\ B)
It only works syntactically!
A, B: Prop
H: A -> BA /\ BA, B: Prop
H: A -> BA /\ id BA, B: Prop
H: A -> BAA, B: Prop
H: A -> BA
Except when it tries to unify things.
Abort.