Systems and
Formalisms Lab

Week 3: Introduction to proof scripting and Ltac

Author

Adam Chlipala, with modifications by CS-428 staff.

License

No redistribution allowed (usage by permission in CS-428).

Ltac Programming Basics

We have already seen a few examples of Ltac programs, without much explanation. Ltac is the proof-scripting language built into Coq. Actually, every primitive step in our proofs has been a (degenerate, small) Ltac program. Let's take a bottom-up look at more Ltac features.

We've seen match goal tactics a few times so far. They allow syntactic inspection of hypothesis and conclusion formulas of current goals, where we pick tactics to run based on what we find. Here's a simple example to find an if and do a case split based on its test expression.

Ltac find_if :=
  match goal with
    | [ |- if ?X then _ else _ ] => destruct X
  end.

Here's a proof that becomes trivial, given find_if. You can imagine a whole family of similar theorems that also become trivial.


forall a b c : bool, if a then if b then True else True else if c then True else True

forall a b c : bool, if a then if b then True else True else if c then True else True
a, b, c: bool

if a then if b then True else True else if c then True else True
c: bool

True
c: bool
True
b: bool
True
b: bool
True
(* Note `repeat` for "run over and over until you can't progress." *)
c: bool

True
b: bool
True
b: bool
True
(* A fun tactic that consults a database of really boring steps. *)
b: bool

True
b: bool
True
b: bool

True
trivial.

Let's write that again with more automation.

  

forall a b c : bool, if a then if b then True else True else if c then True else True
intros; repeat find_if; trivial. Qed.

Another very useful Ltac building block is context patterns.

Ltac find_if_inside :=
  match goal with
    | [ |- context[if ?X then _ else _] ] => destruct X
  end.

The behavior of this tactic is to find any subterm of the conclusion that is an if and then destruct the test expression. This version subsumes find_if. The general behavior of context (an Ltac keyword) is to allow matching in arbitrary subterms.


forall a b c : bool, if a then if b then True else True else if c then True else True

forall a b c : bool, if a then if b then True else True else if c then True else True
intros; repeat find_if_inside; trivial. Qed.

We can also use find_if_inside to prove goals that find_if does not simplify sufficiently.


forall a b : bool, (if a then 42 else 42) = (if b then 42 else 42)

forall a b : bool, (if a then 42 else 42) = (if b then 42 else 42)
intros; repeat find_if_inside; reflexivity. Qed.

Implementing a custom tauto

In class, we develop our own implementation of propositional one feature at a time, but here's just the final product. To understand it, we print the definitions of the logical connectives. Interestingly enough, they are special cases of the machinery we met previously for inductive relations!

Inductive True : Prop := I : True.
Inductive False : Prop := .
Notation "A /\ B" := (and A B) : type_scope (default interpretation)
Inductive and (A B : Prop) : Prop := conj : A -> B -> A /\ B. Arguments and (A B)%type_scope Arguments conj [A B]%type_scope _ _
Notation "A \/ B" := (or A B) : type_scope (default interpretation)
Inductive or (A B : Prop) : Prop := or_introl : A -> A \/ B | or_intror : B -> A \/ B. Arguments or (A B)%type_scope Arguments or_introl [A B]%type_scope _, [_] _ _ Arguments or_intror [A B]%type_scope _, _ [_] _

(Implication (->) is built into Coq, so nothing to look up there.)

Ltac my_tauto :=
  repeat match goal with
	   | [ H : ?P |- ?P ] => exact H

	   | [ |- True ] => constructor
	   | [ |- _ /\ _ ] => constructor
	   | [ |- _ -> _ ] => intro

	   | [ H : False |- _ ] => destruct H
	   | [ H : _ /\ _ |- _ ] => destruct H
	   | [ H : _ \/ _ |- _ ] => destruct H

	   | [ H1 : ?P -> ?Q, H2 : ?P |- _ ] => specialize (H1 H2)
	 end.
Section propositional.
  Variables P Q R : Prop.

  
P, Q, R: Prop

(P \/ Q \/ False) /\ (P -> Q) -> True /\ Q
P, Q, R: Prop

(P \/ Q \/ False) /\ (P -> Q) -> True /\ Q
my_tauto. Qed. End propositional.

Automating the two-stacks queue

Let's experience the process of gradually automating a proof from the last lecture. Here's the code, stripped of comments:

Backtracking

match goal has useful backtracking semantics. When one rule fails, we backtrack automatically to the next one.

For instance, this (unnecessarily verbose) proof script works:


True

True
match goal with | [ |- _ ] => intro | [ |- True ] => constructor end. Qed.

The example shows how failure can move to a different pattern within a match. Failure can also trigger an attempt to find a different way of matching a single pattern. Consider another example:


forall P Q R : Prop, P -> Q -> R -> Q

forall P Q R : Prop, P -> Q -> R -> Q
H1
P, Q, R: Prop
H: P
H0: Q
H1: R

Q

Coq prints "H1". By applying idtac with an argument, a convenient debugging tool for "leaking information out of matches," we see that this match first tries binding H to H1, which cannot be used to prove Q. Nonetheless, the following variation on the tactic succeeds at proving the goal:

  
H1
H0
Qed.

The tactic first unifies H with H1, as before, but exact H fails in that case, so the tactic engine searches for more possible values of H. Eventually, it arrives at the correct value, so that exact H and the overall tactic succeed.

Instantiating quantifiers

Let's try some more ambitious reasoning, with quantifiers. We'll be instantiating quantified facts heuristically. If we're not careful, we get in a loop repeating the same instantiation forever. We'll need a way to check that a fact is not already known. Here's a tactic:

Ltac notHyp P :=
  match goal with
  | [ _ : P |- _ ] =>
      fail 1 (* A hypothesis already asserts this fact. *)
  | _ =>
      match P with
      | ?P1 /\ ?P2 =>
          (* Check each conjunct of `P` separately, since they might
             be known by different means. *)
          first [ notHyp P1 | notHyp P2 | fail 2 ]
      | _ =>
          idtac (* If we manage to get this far, then we found no
                   redundancy, so declare success. *)
      end
  end.

The number for fail N indicates failing at the backtracking point N levels out from where we are. first applies the first tactic that does not fail.

This tactic adds a fact to the context, only if it is not already present.

Ltac extend pf :=
  let t := type of pf in
    notHyp t; pose proof pf.

With these tactics defined, we can write a tactic completer for, among other things, adding to the context all consequences of a set of simple first-order formulas.

Ltac completer :=
  repeat match goal with
    | [ H : _ /\ _ |- _ ] => destruct H
    | [ H : ?P -> ?Q, H' : ?P |- _ ] => specialize (H H')

    | [ H : forall x, ?P x -> _, H' : ?P ?X |- _ ] => extend (H X H')

    | [ |- _ /\ _ ] => constructor
    | [ |- forall x, _ ] => intro
    | [ |- _ -> _ ] => intro (* Note: Redundant with rule above *)
    end.

Section firstorder.
  Variable A : Set.
  Variables P Q R S : A -> Prop.

  Hypothesis H1 : forall x, P x -> Q x /\ R x.
  Hypothesis H2 : forall x, R x -> S x.

  
A: Set
P, Q, R, S: A -> Prop
H1: forall x : A, P x -> Q x /\ R x
H2: forall x : A, R x -> S x

A -> forall x : A, P x -> S x
A: Set
P, Q, R, S: A -> Prop
H1: forall x : A, P x -> Q x /\ R x
H2: forall x : A, R x -> S x

A -> forall x : A, P x -> S x
A: Set
P, Q, R, S: A -> Prop
H1: forall x : A, P x -> Q x /\ R x
H2: forall x : A, R x -> S x
y, x: A
H: P x
H0: Q x
H3: R x
H4: S x

S x
assumption. Qed. End firstorder.

Functional Programming in Ltac

Let's write a list-length function in Ltac rather than Gallina. In class, we'll muddle through some intermediate versions before getting to the first version that at least parses.

Module Import FirstTry.
  Ltac length ls :=
    match ls with
    | nil => O
    | _ :: ?ls' => constr:(S (length ls'))
    end.
End FirstTry.


False
n:= S (length [2; 3]): nat

False
Abort.

Something went wrong there.

Ltac length ls :=
  match ls with
    | nil => O
    | _ :: ?ls' =>
      let ls'' := length ls' in
        constr:(S ls'')
  end.


False
n:= 3: nat

False
Abort.

Here's a map implementation in Ltac. Strangely, it needs to be passed the type of the new list explicitly.

Ltac map T f :=
  let rec map' ls :=
    match ls with
      | nil => constr:(@nil T)
      | ?x :: ?ls' =>
        let x' := f x in
          let ls'' := map' ls' in
            constr:(x' :: ls'')
    end in
  map'.


False
l:= [(1, 1); (2, 2); (3, 3)]: list (nat * nat)

False
Abort.

Now let's revisit length and see how we might implement "printf debugging" for it.

Module Import WithPrinting.
  Ltac length ls :=
    idtac ls;
    match ls with
    | nil => O
    | _ :: ?ls' =>
      let ls'' := length ls' in
      constr:(S ls'')
    end.
End WithPrinting.


False
variable n should be bound to a term.

Oh, that has a dynamic type error.

Abort.

The problem is that Ltac as a language contains several datatypes. One of them is "tactic sequence," which can't be mixed with other datatypes like "term in the logic." Tactic sequences don't return results. We can use continuation-passing style as a mitigation.

Module Import WithPrintingFixed.
  Ltac length ls k :=
    idtac ls;
    match ls with
    | nil => k O
    | _ :: ?ls' => length ls' ltac:(fun n => k (S n))
    end.
End WithPrintingFixed.


False
[1; 2; 3]
[2; 3]
[3]
[]
Abort.

However, it's not always convenient to use continuation-passing style everywhere, so cool kids use the following hack to sneak side effects into otherwise-functional Ltac code.

Module Import WithPrintingFixedWithoutContinuations.
  Ltac length ls :=
    let __ := match constr:(Set) with
              | _ => (* put all your side effects here:*)
                     idtac ls
              end in
    match ls with
    | nil => constr:(O)
    | _ :: ?ls' => let L := length ls' in constr:(S L)
    end.
End WithPrintingFixedWithoutContinuations.


False
[1; 2; 3]
[2; 3]
[3]
[]
Abort.

Creating Unification Variables

A final useful ingredient in tactic crafting is the ability to allocate new unification variables explicitly. Before we are ready to write a tactic, we can try out its ingredients one at a time.


(forall x : nat, S x > x) -> 2 > 1

(forall x : nat, S x > x) -> 2 > 1
H: forall x : nat, S x > x

2 > 1
H: forall x : nat, S x > x
y:= ?y: nat

2 > 1
H: S ?y > ?y

2 > 1
apply H. Qed. Ltac newEvar T k := let x := fresh "x" in evar (x : T); let x' := eval unfold x in x in clear x; k x'. Ltac insterU H := repeat match type of H with | forall x : ?T, _ => newEvar T ltac:(fun y => specialize (H y)) end.

(forall x : nat, S x > x) -> 2 > 1

(forall x : nat, S x > x) -> 2 > 1
H: forall x : nat, S x > x

2 > 1
H: S ?x > ?x

2 > 1
apply H. Qed.

This particular example is somewhat silly, since apply by itself would have solved the goal originally. Separate forward reasoning is more useful on hypotheses that end in existential quantifications. Before we go through an example, it is useful to define a variant of insterU that does not clear the base hypothesis we pass to it.

Ltac insterKeep H :=
  let H' := fresh "H'" in
    pose proof H as H'; insterU H'.

Section t6.
  Variables A B : Type.
  Variable P : A -> B -> Prop.
  Variable f : A -> A -> A.
  Variable g : B -> B -> B.

  Hypothesis H1 : forall v, exists u, P v u.
  Hypothesis H2 : forall v1 u1 v2 u2,
    P v1 u1
    -> P v2 u2
    -> P (f v1 v2) (g u1 u2).

  
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)

forall v1 v2 : A, exists u1 u2 : B, P (f v1 v2) (g u1 u2)
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)

forall v1 v2 : A, exists u1 u2 : B, P (f v1 v2) (g u1 u2)
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A

exists u1 u2 : B, P (f v1 v2) (g u1 u2)
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
H': exists u : B, P ?x u
H'0: exists u : B, P ?x0 u

exists u1 u2 : B, P (f v1 v2) (g u1 u2)
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
x0: B
p: P ?x x0
x: B
H: P ?x0@{H':=ex_intro (fun u : B => P ?x u) x0 p} x

exists u1 u2 : B, P (f v1 v2) (g u1 u2)
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
x0: B
p: P ?x x0
x: B
H: P ?x0@{H':=ex_intro (fun u : B => P ?x u) x0 p} x

exists u2 : B, P (f v1 v2) (g ?u1 u2)
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
x0: B
p: P ?x x0
x: B
H: P ?x0@{H':=ex_intro (fun u : B => P ?x u) x0 p} x

P (f v1 v2) (g ?u1 ?u2)
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
x0: B
p: P ?x x0
x: B
H: P ?x0@{H':=ex_intro (fun u : B => P ?x u) x0 p} x

P v1 ?u1
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
x0: B
p: P ?x x0
x: B
H: P ?x0@{H':=ex_intro (fun u : B => P ?x u) x0 p} x
P v2 ?u2
A, B: Type
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
x0: B
p: P ?x x0
x: B
H: P v1 x

P v2 ?u2
exact p.

Next week, we'll meet eauto, which can do these last steps automatically.

  Qed.
End t6.

Here's an example where something bad happens.

Section t7.
  Variables A B : Type.
  Variable Q : A -> Prop.
  Variable P : A -> B -> Prop.
  Variable f : A -> A -> A.
  Variable g : B -> B -> B.

  Hypothesis H1 : forall v, Q v -> exists u, P v u.
  Hypothesis H2 : forall v1 u1 v2 u2,
    P v1 u1
    -> P v2 u2
    -> P (f v1 v2) (g u1 u2).

  
A, B: Type
Q: A -> Prop
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, Q v -> exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)

forall v1 v2 : A, Q v1 -> Q v2 -> exists u1 u2 : B, P (f v1 v2) (g u1 u2)
A, B: Type
Q: A -> Prop
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, Q v -> exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)

forall v1 v2 : A, Q v1 -> Q v2 -> exists u1 u2 : B, P (f v1 v2) (g u1 u2)
intros; do 2 insterKeep H1; repeat match goal with | [ H : ex _ |- _ ] => destruct H end; eauto.

Oh, two trivial goals remain.

    
A, B: Type
Q: A -> Prop
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, Q v -> exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
H: Q v1
H0: Q v2
H': Q v2 -> exists u : B, P v2 u

x0
Q v2
A, B: Type
Q: A -> Prop
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, Q v -> exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
H: Q v1
H0: Q v2
H': exists u : B, P v2 u
H'0: Q v1 -> exists u : B, P v1 u
Q v1
A, B: Type
Q: A -> Prop
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, Q v -> exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
H: Q v1
H0: Q v2
H': exists u : B, P v2 u
H'0: Q v1 -> exists u : B, P v1 u

x2
Q v1
assumption. Qed. End t7.

Why did we need to do that extra work? The forall rule was also matching implications!

Module Import FixedInster.
  Ltac insterU tac H :=
    repeat match type of H with
           | forall x : ?T, _ =>
             match type of T with
             | Prop =>
               (let H' := fresh "H'" in
                assert (H' : T) by solve [ tac ];
                specialize (H H'); clear H')
               || fail 1
             | _ =>
               newEvar T ltac:(fun y => specialize (H y))
             end
           end.

  Ltac insterKeep tac H :=
    let H' := fresh "H'" in
      pose proof H as H'; insterU tac H'.
End FixedInster.

Section t7'.
  Variables A B : Type.
  Variable Q : A -> Prop.
  Variable P : A -> B -> Prop.
  Variable f : A -> A -> A.
  Variable g : B -> B -> B.

  Hypothesis H1 : forall v, Q v -> exists u, P v u.
  Hypothesis H2 : forall v1 u1 v2 u2,
    P v1 u1
    -> P v2 u2
    -> P (f v1 v2) (g u1 u2).

  
A, B: Type
Q: A -> Prop
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, Q v -> exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)

forall v1 v2 : A, Q v1 -> Q v2 -> exists u1 u2 : B, P (f v1 v2) (g u1 u2)
A, B: Type
Q: A -> Prop
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, Q v -> exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)

forall v1 v2 : A, Q v1 -> Q v2 -> exists u1 u2 : B, P (f v1 v2) (g u1 u2)
A, B: Type
Q: A -> Prop
P: A -> B -> Prop
f: A -> A -> A
g: B -> B -> B
H1: forall v : A, Q v -> exists u : B, P v u
H2: forall (v1 : A) (u1 : B) (v2 : A) (u2 : B), P v1 u1 -> P v2 u2 -> P (f v1 v2) (g u1 u2)
v1, v2: A
H: Q v1
H0: Q v2

exists u1 u2 : B, P (f v1 v2) (g u1 u2)
do 2 insterKeep ltac:(idtac; match goal with | [ H : Q ?v |- _ ] => match goal with | [ _ : context[P v _] |- _ ] => fail 1 | _ => apply H end end) H1; repeat match goal with | [ H : ex _ |- _ ] => destruct H end; eauto. Qed. End t7'.

One more example of working with existentials:


exists p : nat * nat, fst p = 3

exists p : nat * nat, fst p = 3

fst ?p = 3

fst (3, 2) = 3

We use instantiate to plug in a value for one of the "question-mark variables" in the conclusion. The 1 := part says "first such variable mentioned in the conclusion, counting from right to left."

  reflexivity.
Qed.

A way that plays better with automation:


exists p : nat * nat, fst p = 3

exists p : nat * nat, fst p = 3
econstructor; match goal with | [ |- fst ?x = 3 ] => unify x (3, 2) end; reflexivity. Qed.