Meta-programming
- Files:
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 "!".Sequencing and backtracking
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
Qed.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. Qed.
Exercise: Backtracking
For each of the following scripts, predict without running Rocq whether it
succeeds, and which goal remains or which witness is chosen.
Then check your answer, using Show Proof to see the witness.
The try are only there so that the file compiles whatever the answer.
exists b : bool, negb b = truetry (unshelve eexists ; constructor). Abort.exists b : bool, negb b = trueexists n : nat, n = 1try (unshelve eexists ; constructor). Abort.exists n : nat, n = 1True \/ (False -> False)True \/ (False -> False)Abort.x: FalseFalseTrue \/ (False -> False)True \/ (False -> False)TrueAbort.True(False -> False) \/ Truetry (constructor ; exact I). Abort.(False -> False) \/ True
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
Inductive mult4 : nat -> Prop := | mult4_0 : mult4 0 | mult4_plus n : mult4 n -> mult4 (4 + n). Inductive not_mult4 : nat -> Prop := | not_mult4_1 : not_mult4 1 | not_mult4_2 : not_mult4 2 | not_mult4_3 : not_mult4 3 | not_mult4_plus n : not_mult4 n -> not_mult4 (4 + n).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. Qed.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
Exercise: Tacticals
Prove each of the following goals with a single sentence, that is one tactic
expression ending with a single . after Proof..
Combine ;, [ ... | ... ], .., try, solve, repeat and do.
mult4 12 /\ not_mult4 7 /\ mult4 0 /\ not_mult4 10Admitted.mult4 12 /\ not_mult4 7 /\ mult4 0 /\ not_mult4 10forall n : nat, not_mult4 n -> not_mult4 (8 + n)Admitted.forall n : nat, not_mult4 n -> not_mult4 (8 + n)forall n : nat, n < 3 -> not_mult4 (n + 1)Admitted.forall n : nat, n < 3 -> not_mult4 (n + 1)forall n : nat, mult4 n -> mult4 (n + 4)Admitted.forall n : nat, mult4 n -> mult4 (n + 4)
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.Qed.
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. *) end.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.As a reminder, here is a tactic defined with match goal.
Ltac my_assumption := match goal with | H : ?A |- ?A => apply H end.forall P Q R : Type, P -> Q -> R -> Pforall P Q R : Type, P -> Q -> R -> Pmy_assumption. Qed.P, Q, R: Type
X: P
X0: Q
X1: RP
Exercise: Repeated split
Define a tactic splits to perform split repeatedly on goals of the shape
A * B but not on any other kinds of goals (for instance it should not solve
True).
For instance, on (nat * bool) * (unit * bool) it should yield 4 subgoals.
Hint: You will need split and match goal or lazymatch goal and idtac.
Try it on these goals.
nat * boolAdmitted.nat * boolnat * (bool * unit) * (unit * nat * bool) * unitAdmitted.nat * (bool * unit) * (unit * nat * bool) * unit
The clear tactic
It is possible to forget about hypotheses using the clear tactic as
follows.
It is sometimes useful before applying the induction tactic to get rid of
something in the generated induction hypothesis.
forall P Q R : Type, P -> Q -> R -> Pforall P Q R : Type, P -> Q -> R -> PP, Q, R: Type
p: P
q: Q
r: RPP, R: Type
r: RPP, R: Type
r: RPP: TypePP: TypeFalseAbort.False
Exercise: Clearing before induction
Prove the following by induction on n.
In particular, don't use lia, we want you to use the induction hypothesis.
forall n : nat, n mod 14 = 7 -> n - n = 0Admitted.forall n : nat, n mod 14 = 7 -> n - n = 0
Matching on the type of an expression
The same way we can match on the goal, we can match the type of an expression
using the type of construct.
Ltac handle_hyp h := lazymatch type of h with | (?A * ?B)%type => destruct h | nat => induction h | _ => idtac end.forall n : nat * nat, fst n = snd nforall n : nat * nat, fst n = snd nn: nat * natfst n = snd nn, n0: natfst (n, n0) = snd (n, n0)Abort.n0: natfst (0, n0) = snd (0, n0)n, n0: nat
IHn: fst (n, n0) = snd (n, n0)fst (S n, n0) = snd (S n, n0)
The fresh tactic
Not all tactics operate on goals, some are used to produce values, or even names (as in variable names).
This is the case of the fresh tactic which you can use to get a fresh
name (as in not used in the currrent context) to give to a newly introduced
variable.
forall n m k : nat, n + m - k = m + n - kforall n m k : nat, n + m - k = m + n - kH: natforall m k : nat, H + m - k = m + H - kH, n: natforall k : nat, H + n - k = n + H - kH, n, n0: natH + n - n0 = n + H - n0(* It picked [H0] *) Abort.n, n0: nat0 + n - n0 = n + 0 - n0H0, n, n0: natS H0 + n - n0 = n + S H0 - n0
Exercise: Forward reasoning
A very useful tactic that should be part of the standard library, but is not,
is the fwd tactic.
Given H : P -> Q, fwd H will yield two goals:
one where
Pmust be provenand one where
Hhas been replaced byH : Q
Note that there shouldn't be anything else appearing in the goal.
Try on the following goal.
Do not use apply, only intros, fwd and assumption.
forall P Q R : Type, (P -> Q -> R) -> (P -> Q) -> P -> RAdmitted.forall P Q R : Type, (P -> Q -> R) -> (P -> Q) -> P -> R
Exercise: Forward reasoning with a tactic (harder)
Try to generalise the code for fwd so that you can also provide a tactic.
If H : P -> Q then forward_gen H tac will also apply tac to try and
solve the goal P first.
Once forward_gen is defined, we define handy notations:
Tactic Notation "forward" constr(H) := forward_gen H ltac:(idtac). and
Tactic Notation "forward" constr(H) "by" tactic(tac) := forward_gen H tac.
forall P Q R : Type, (P -> Q -> R) -> (P -> Q) -> P -> RAdmitted.forall P Q R : Type, (P -> Q -> R) -> (P -> Q) -> P -> R
Exercise: A tauto tactic
Write a tactic my_tauto that solves the following goals.
You can start from the following, and expand it:
Ltac my_tauto := lazymatch goal with | |- True => constructor end.
Section Tauto. Variables (P Q R : Prop).P, Q, R: PropP -> True /\ TrueAdmitted.P, Q, R: PropP -> True /\ TrueP, Q, R: PropP -> PAdmitted.P, Q, R: PropP -> PP, Q, R: PropP -> (P -> R) -> RAdmitted.P, Q, R: PropP -> (P -> R) -> RP, Q, R: PropFalse \/ TrueAdmitted.P, Q, R: PropFalse \/ TrueP, Q, R: Prop(P \/ Q \/ False) /\ (P -> Q) -> True /\ (R \/ Q)Admitted. End Tauto.P, Q, R: Prop(P \/ Q \/ False) /\ (P -> Q) -> True /\ (R \/ Q)