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.


bool

bool
constructor.
true

It picks true

Unnamed_thm is defined

exists b : bool, b = false

exists b : bool, b = false

unshelve eexists lets us give the witness first

  

bool

?b = false

bool
constructor.

true = false

We're stuck now

    
The command has indeed failed with message: Unable to unify "false" with "true".

true = false
Abort.

exists b : bool, b = false

exists b : bool, b = false
unshelve eexists ; constructor.
(ex_intro (fun b : bool => b = false) false eq_refl)

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 = false

exists 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.
Unnamed_thm0 is defined

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

mult4 is defined
mult4_ind is defined
mult4_sind is defined
not_mult4 is defined
not_mult4_ind is defined
not_mult4_sind is defined

forall n : nat, mult4 n -> not_mult4 n -> False

forall n : nat, mult4 n -> not_mult4 n -> False
n: nat
h: mult4 n
hn: not_mult4 n

False
h: mult4 1

False
h: mult4 2
False
h: mult4 3
False
n: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> False
False
h: mult4 1

False
inversion h.
h: mult4 2

False
inversion h.
h: mult4 3

False
inversion h.
n: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> False

False
n: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> False
n0: nat
H0: mult4 n
H: n0 = n

False
n: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> False
H0: mult4 n

False
auto. Abort.

forall n : nat, mult4 n -> not_mult4 n -> False

forall 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.

  
n: nat
h: mult4 n
hn: not_mult4 n

False
induction hn ; [ inversion h | inversion h | inversion h | inversion h ; subst ; auto ]. Abort.

forall n : nat, mult4 n -> not_mult4 n -> False

forall n : nat, mult4 n -> not_mult4 n -> False

In fact we can even factorise the three first cases

  
n: nat
h: mult4 n
hn: not_mult4 n

False
induction hn ; [ inversion h .. | inversion h ; subst ; auto ].

The elipsis represented by .. can be used at most once, and repeats the preceding tactic.

Abort.

forall n : nat, mult4 n -> not_mult4 n -> False

forall n : nat, mult4 n -> not_mult4 n -> False
n: nat
h: mult4 n
hn: not_mult4 n

False

We can put it in the middle for instance

  
h: mult4 2

False
h: mult4 3
False

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 -> False

forall n : nat, mult4 n -> not_mult4 n -> False

In fact idtac can even be left out

  
n: nat
h: mult4 n
hn: not_mult4 n

False
h: mult4 2

False
h: mult4 3
False
n: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> False
False
Abort.

forall n : nat, mult4 n -> not_mult4 n -> False

forall 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 n

False
n: nat
h: mult4 n
hn: not_mult4 n

False

It does nothing because it failed.

  
h: mult4 1

False
h: mult4 2
False
h: mult4 3
False
n: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> False
False

We can combine it with solve to make sure we only use inversion if it solves the goal.

  
n: nat
h: mult4 (4 + n)
hn: not_mult4 n
IHhn: mult4 n -> False

False
Abort.

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 40

mult4 40

mult4 36

mult4 32

mult4 28

mult4 24

No way we're doing this forever

  

mult4 0
apply mult4_0. Abort.

mult4 40

mult4 40

mult4 8

mult4 0
Abort.

mult4 40

mult4 40
repeat constructor.
Unnamed_thm1 is defined

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 * unit

nat * bool * unit
A':= = (nat * bool)%type: Set
B':= = unit: Set

nat * 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

The command has indeed failed with message: Tactic failure: This is expected this time.
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

  
The command has indeed failed with message: No matching clauses for match.
A':= = (nat * bool)%type: Set
B':= = unit: Set
A'':= = nat: Set

nat * 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.
  
(let A' := (nat * bool)%type in let B' := unit in let A'' := nat in (13, false, tt))
Unnamed_thm2 is defined

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.

split_or_apply is defined

forall A B : Prop, (A -> B) -> A /\ B

forall A B : Prop, (A -> B) -> A /\ B
A, B: Prop
H: A -> B

A /\ B

No need to specify the names we won't be using them.

  
A, B: Prop
H: A -> B

A
A, B: Prop
H: A -> B
B
A, B: Prop
H: A -> B

A
The command has indeed failed with message: No matching clauses for match.
A, B: Prop
H: A -> B

A
admit.
A, B: Prop
H: A -> B

B
A, B: Prop
H: A -> B

A
Abort.

forall A B : Prop, (A -> B) -> A /\ B

forall A B : Prop, (A -> B) -> A /\ B
A, B: Prop
H: A -> B

A /\ B
A, B: Prop
H: A -> B

id (A /\ B)
A, B: Prop
H: A -> B

id (A /\ B)

It only works syntactically!

  
A, B: Prop
H: A -> B

A /\ B
A, B: Prop
H: A -> B

A /\ id B
A, B: Prop
H: A -> B

A
A, B: Prop
H: A -> B
A

Except when it tries to unify things.

Abort.