Proof techniques crash course

Every proof you’ll ever write is some mix of a small number of standard moves. This chapter is a fast reference: each section names a classic technique, says how it maps onto an Algae rule, and then hands you three exercises to drill it.

Each exercise is a live editor seeded with a hole. Press Check ▶ to see the open goal, click the hole (or press Ctrl-Space) for suggestions, and read the hint if you’re stuck. Everything here uses only core — the rules you met in Inference rules, Reasoning backward, and the tour (The logical toolkit, Quantifiers and ⇔).

Here’s one worked all the way through, so you know what a finished answer looks like — the and-introduction from Reasoning backward:

rule and_intro(P Q : Prop)
   P;
   Q
  ────────────────────────
   P  Q
end;

lemma both(A B : Prop, x := A, y := B)
   A  B;
proof
  by and_intro(A, B) cases
    case x := A, y := B  A; by x; qed;
    case x := A, y := B  B; by y; qed;
  qed;
qed;

Note the shape of each case: it restates the hypotheses it inherits (x := A, y := B) before its . Every case and every then states its context in full — Algae does no implicit weakening, so an assumption you don’t carry forward is one you discarded. Expect to write your lemma parameters out again in each branch of the exercises below.

Now the techniques.

1. Direct proof

Goal: prove P. Method: apply a rule whose conclusion matches P, then discharge whatever it leaves from your assumptions. A premise-free step (an axiom or an assumption by h) closes a goal outright.

Exercises

1a. From a proof of A, build A B (the left injection).

rule or_intro_left(P Q : Prop)
   P
  ────────────────────────
   P  Q
end;

lemma inject_left(A B : Prop, x := A)
   A  B;
proof
  by wip(?goal);
wip;

Hint

or_intro_left(A, B) proves A B from A — one premise, so then x := A A;, closed by x.

1b. Same, but the right injection: from a proof of B, build A B.

rule or_intro_right(P Q : Prop)
   Q
  ────────────────────────
   P  Q
end;

lemma inject_right(A B : Prop, y := B)
   A  B;
proof
  by wip(?goal);
wip;

Hint

or_intro_right(A, B) proves A B from B. then y := B B; then by y.

1c. Take a conjunction apart: from a proof of A B, get A.

rule and_left(P Q : Prop)
   P  Q
  ────────────────────────
   P
end;

lemma take_left(A B : Prop, h := A  B)
   A;
proof
  by wip(?goal);
wip;

Hint

and_left(A, B) concludes A from its one premise A B. then h := A B A B; and close by h.

2. Conditional proof (implication)

Goal: prove P Q. Method: assume P, then prove Q. implication_intro does exactly this — it introduces the antecedent as a named hypothesis after its premise P := P Q.

Exercises

2a. Prove the identity implication A A.

rule implication_intro(P Q : Prop)
  P := P  Q
  ────────────────────────
   P  Q
end;

lemma id(A : Prop)
   A  A;
proof
  by wip(?goal);
wip;

Hint

by implication_intro(A, A) then P := A A; leaves you assuming P : A and proving A — close it by P.

2b. If you already hold a proof of B, then A B for any A.

rule implication_intro(P Q : Prop)
  P := P  Q
  ────────────────────────
   P  Q
end;

lemma const_imp(A B : Prop, q := B)
   A  B;
proof
  by wip(?goal);
wip;

Hint

implication_intro(A, B) then q := B, P := A B; — ignore P and close by q.

2c. Prove (A B) A — assume the conjunction, then project.

rule implication_intro(P Q : Prop)
  P := P  Q
  ────────────────────────
   P  Q
end;

rule and_left(P Q : Prop)
   P  Q
  ────────────────────────
   P
end;

lemma proj(A B : Prop)
   (A  B)  A;
proof
  by wip(?goal);
wip;

Hint

implication_intro(A B, A) then P := A B A; then by and_left(A, B) then P := A B A B; by P;.

3. Proof by cases

Goal: prove P. Method: split into all possible cases and prove P in each. When the “all possible cases” is a disjunction A B you have, or_elim is the tool: it gives you two branches, one assuming A and one assuming B, both aiming at the same goal.

Exercises

3a. Disjunction commutes: from A B, prove B A.

rule or_elim(P Q R : Prop)
   P  Q;
  P := P  R;
  Q := Q  R
  ────────────────────────
   R
end;

rule or_intro_left(P Q : Prop)
   P
  ────────────────────────
   P  Q
end;

rule or_intro_right(P Q : Prop)
   Q
  ────────────────────────
   P  Q
end;

lemma or_comm(A B : Prop, d := A  B)
   B  A;
proof
  by wip(?goal);
wip;

Hint

by or_elim(A, B, B A) cases gives three goals: re-prove A B (by d), then a branch P := A (build B A with or_intro_right) and a branch Q := B (with or_intro_left).

3b. A trivial-looking but instructive one: from A A, prove A.

rule or_elim(P Q R : Prop)
   P  Q;
  P := P  R;
  Q := Q  R
  ────────────────────────
   R
end;

lemma idem(A : Prop, d := A  A)
   A;
proof
  by wip(?goal);
wip;

Hint

or_elim(A, A, A) cases — feed d for the disjunction, and each branch hands you an assumption of A to close with.

3c. The constructive dilemma: from A B, A C and B C, conclude C.

rule or_elim(P Q R : Prop)
   P  Q;
  P := P  R;
  Q := Q  R
  ────────────────────────
   R
end;

rule implication_elim(P Q : Prop)
   P  Q;
   P
  ────────────────────────
   Q
end;

lemma dilemma(A B C : Prop, d := A  B, f := A  C, g := B  C)
   C;
proof
  by wip(?goal);
wip;

Hint

or_elim(A, B, C) cases. In the P := A branch run implication_elim(A, C) against f and P; symmetrically use g and Q in the Q := B branch.

4. Proof by contradiction

Goal: prove ¬P. Method: assume P, and derive absurdity (False). That’s negation_intro — it assumes P and asks you to reach False. Two more tools travel with it: negation_elim turns a proof of P and ¬P into False, and false_elim turns False into anything at all (the principle of explosion).

Algae’s logic is intuitionistic

Classic “proof by contradiction” sometimes means proving a positive P by assuming ¬P and deriving False. That step needs the law of excluded middle, which core deliberately does not ship — so here contradiction proves negations (¬P), and explosion carries a contradiction to any goal. (This is also why the next section has no exercises.)

Exercises

5a. From A False, prove ¬A.

rule negation_intro(P : Prop)
  P := P  False
  ────────────────────────
   ¬P
end;

rule implication_elim(P Q : Prop)
   P  Q;
   P
  ────────────────────────
   Q
end;

lemma neg_from_imp(A : Prop, f := A  False)
   ¬A;
proof
  by wip(?goal);
wip;

Hint

negation_intro(A) then f := A False, P := A False; then run implication_elim(A, False) against f and P.

5b. Explosion: from a proof of A and a proof of ¬A, prove any C.

rule false_elim(P : Prop)
   False
  ────────────────────────
   P
end;

rule negation_elim(P : Prop)
   P;
   ¬P
  ────────────────────────
   False
end;

lemma explode(A C : Prop, x := A, nx := ¬A)
   C;
proof
  by wip(?goal);
wip;

Hint

false_elim(C) then x := A, nx := ¬A False; reduces the goal to False, which negation_elim(A) produces from x and nx (two cases).

5c. Prove ¬¬A from A.

rule negation_intro(P : Prop)
  P := P  False
  ────────────────────────
   ¬P
end;

rule negation_elim(P : Prop)
   P;
   ¬P
  ────────────────────────
   False
end;

lemma dni(A : Prop, x := A)
   ¬(¬A);
proof
  by wip(?goal);
wip;

Hint

negation_intro(¬A) then x := A, P := ¬A False; — now you hold P : ¬A and x : A, a contradiction, so negation_elim(A) gives False.

5. Proof by contrapositive

Goal: prove P Q. Method: prove ¬Q ¬P instead. core provides this as the contrapositive rule: it takes your goal P Q down to the single premise ¬Q ¬P.

This one is classical

Unlike everything else in this chapter, contrapositive is a classical principle — turning ¬Q ¬P back into P Q rests on the law of excluded middle. core ships it as a primitive rule so the technique is available; the rest of the logic stays intuitionistic.

Exercises

6a. From ¬B ¬A, prove A B.

rule contrapositive(P Q : Prop)
   ¬Q  ¬P
  ────────────────────────
   P  Q
end;

lemma contra_direct(A B : Prop, nn := ¬B  ¬A)
   A  B;
proof
  by wip(?goal);
wip;

Hint

by contrapositive(A, B) then nn := ¬B ¬A ¬B ¬A; by nn; — the rule turns the goal straight into the hypothesis you hold.

6b. The mirror: from ¬A ¬B, prove B A.

rule contrapositive(P Q : Prop)
   ¬Q  ¬P
  ────────────────────────
   P  Q
end;

lemma contra_swap(A B : Prop, nn := ¬A  ¬B)
   B  A;
proof
  by wip(?goal);
wip;

Hint

by contrapositive(B, A) then nn := ¬A ¬B ¬A ¬B; by nn; — mind the order: the goal is B A, so P = B and Q = A.

6c. Prove A A the long way round — via its contrapositive.

rule contrapositive(P Q : Prop)
   ¬Q  ¬P
  ────────────────────────
   P  Q
end;

rule implication_intro(P Q : Prop)
  P := P  Q
  ────────────────────────
   P  Q
end;

lemma contra_id(A : Prop)
   A  A;
proof
  by wip(?goal);
wip;

Hint

contrapositive(A, A) leaves ¬A ¬A — an identity implication you close with implication_intro(¬A, ¬A) then P := ¬A ¬A; by P;.

6. Universal proof

Goal: prove x. P(x). Method: let x be arbitrary (a fresh eigenvariable), then prove P(x). forall_intro hands you that fresh x in its single premise; because x was arbitrary, proving P(x) proves it for all.

Exercises

7a. Everything equals itself: prove x. x = x.

rule forall_intro(T : Sort, P : T  Prop)
  x : T  P(x)
  ────────────────────────
    (x : T) st P(x)
end;

axiom refl(T : Sort, x : T)   x = x;

sort T : Sort;

lemma refl_all
    (x : T) st x = x;
proof
  by wip(?goal);
wip;

Hint

forall_intro(T, _ = _) then x : T x = x; carries a fresh x in; close by refl(T, x).

7b. Instantiate a universal: from y. P(y) and a point a, get P(a).

rule forall_elim(T : Sort, P : T  Prop, x : T)
    (y : T) st P(y)
  ────────────────────────
   P(x)
end;

sort T : Sort;

lemma at_point(P : T  Prop, a : T, all :=  (y : T) st P(y))
   P(a);
proof
  by wip(?goal);
wip;

Hint

forall_elim(T, P, a), then restate all in the continuation — then all := (y : T) st P(y) (y : T) st P(y); — and close by all.

7c. From x. P(x) and x. Q(x), prove x. P(x) Q(x).

rule forall_intro(T : Sort, P : T  Prop)
  x : T  P(x)
  ────────────────────────
    (x : T) st P(x)
end;

rule forall_elim(T : Sort, P : T  Prop, x : T)
    (y : T) st P(y)
  ────────────────────────
   P(x)
end;

rule and_intro(P Q : Prop)
   P;
   Q
  ────────────────────────
   P  Q
end;

sort T : Sort;

lemma forall_and(P Q : T  Prop, hp :=  (x : T) st P(x), hq :=  (x : T) st Q(x))
    (x : T) st P(x)  Q(x);
proof
  by wip(?goal);
wip;

Hint

forall_intro for a fresh x, then and_intro(P(x), Q(x)); get each half by forall_elim on hp / hq at x.

7. Existential proof (a witness)

Goal: prove x. P(x). Method: supply a specific witness a and prove P(a). exists_intro takes the witness as an argument and leaves you the single goal P(a).

Exercises

8a. From a proof of P(a), conclude x. P(x).

rule exists_intro(T : Sort, P : T  Prop, x : T)
   P(x)
  ────────────────────────
    (x : T) st P(x)
end;

sort T : Sort;

lemma witnessed(P : T  Prop, a : T, pa := P(a))
    (x : T) st P(x);
proof
  by wip(?goal);
wip;

Hint

exists_intro(T, P, a) then pa := P(a) P(a); by pa; — you offered a as the witness, so the leftover goal is P at a.

8b. Something exists that equals a: prove x. x = a.

rule exists_intro(T : Sort, P : T  Prop, x : T)
   P(x)
  ────────────────────────
    (x : T) st P(x)
end;

axiom refl(T : Sort, x : T)   x = x;

sort T : Sort;

lemma exists_eq(a : T)
    (x : T) st x = a;
proof
  by wip(?goal);
wip;

Hint

Use a itself as the witness: exists_intro(T, λ (z : T) st z = a, a) then a = a; and close by refl(T, a).

8c. Every element is some self-equal thing: prove z. z = z.

rule exists_intro(T : Sort, P : T  Prop, x : T)
   P(x)
  ────────────────────────
    (x : T) st P(x)
end;

axiom refl(T : Sort, x : T)   x = x;

sort T : Sort;

lemma exists_self(a : T)
    (z : T) st z = z;
proof
  by wip(?goal);
wip;

Hint

Witness with a: exists_intro(T, λ (z : T) st z = z, a) then a = a; then by refl(T, a).

8. Existential elimination (use a witness)

Goal: conclude Q given x. P(x). Method: introduce a fresh witness a with P(a) in hand, and finish the proof using it — but Q must not mention a, since you don’t get to know which witness you were handed. exists_elim gives you that fresh x and the hypothesis witness := P(x).

Exercises

9a. Unpack and immediately repack: from x. P(x), prove x. P(x).

rule exists_intro(T : Sort, P : T  Prop, x : T)
   P(x)
  ────────────────────────
    (x : T) st P(x)
end;

rule exists_elim(T : Sort, P : T  Prop, Q : Prop)
    (x : T) st P(x);
  x : T, witness := P(x)  Q
  ────────────────────────
   Q
end;

sort T : Sort;

lemma repack(P : T  Prop, ex :=  (x : T) st P(x))
    (x : T) st P(x);
proof
  by wip(?goal);
wip;

Hint

exists_elim(T, P, (x : T) st P(x)) cases — feed ex for the existential, then in the witness branch (context ex := (x : T) st P(x), x : T, witness := P(x)) rebuild with exists_intro(T, P, x), carrying that same context into the then and closing by witness.

9b. Flip an existential equation: from x. x = a, prove y. a = y.

rule exists_intro(T : Sort, P : T  Prop, x : T)
   P(x)
  ────────────────────────
    (x : T) st P(x)
end;

rule exists_elim(T : Sort, P : T  Prop, Q : Prop)
    (x : T) st P(x);
  x : T, witness := P(x)  Q
  ────────────────────────
   Q
end;

rule symmetry(T : Sort, x y : T)
   x = y
  ────────────────────────
   y = x
end;

sort T : Sort;

lemma exists_flip(a : T, ex :=  (x : T) st x = a)
    (y : T) st a = y;
proof
  by wip(?goal);
wip;

Hint

Open ex with exists_elim; in the witness branch you hold witness := x = a. Offer x as the new witness (exists_intro(T, λ (y : T) st a = y, x), leaving a = x) and flip with symmetry(T, x, a), leaving x = a, closed by witness. Both then s carry the branch context ex := …, x : T, witness := x = a.

9c. Discharge under a universal: from x. P(x) and x. P(x) R, prove R.

rule exists_elim(T : Sort, P : T  Prop, Q : Prop)
    (x : T) st P(x);
  x : T, witness := P(x)  Q
  ────────────────────────
   Q
end;

rule forall_elim(T : Sort, P : T  Prop, x : T)
    (y : T) st P(y)
  ────────────────────────
   P(x)
end;

rule implication_elim(P Q : Prop)
   P  Q;
   P
  ────────────────────────
   Q
end;

sort T : Sort;

lemma exists_use(P : T  Prop, R : Prop, ex :=  (x : T) st P(x),
                 use :=  (x : T) st P(x)  R)
   R;
proof
  by wip(?goal);
wip;

Hint

In the witness branch (x : T; witness := P(x)) instantiate use at x with forall_elim to get P(x) R, then implication_elim against witness.

9. Equational reasoning (rewriting)

Goal: a goal built from an equation. Method: given a = b, replace a with b (or b with a) somewhere in the goal — that’s forward / backward from Reflexivity and rewriting, and refl closes anything of the form x = x.

Exercises

10a. Close a definitional equation: prove 0 + 0 = 0.

sort Nat : Sort;
op 0 :  Nat;
op s : Nat  Nat;
op + : Nat * Nat  Nat;
axiom add_zero_left(n : Nat)      0 + n = n;
axiom add_succ_left(n m : Nat)    s(n) + m = s(n + m);

lemma zero_plus_zero
   0 + 0 = 0;
proof
  by wip(?goal);
wip;

Hint

0 + 0 = 0 is the axiom add_zero_left at 0: by add_zero_left(0);.

10b. Rewrite, then reflect: prove n = 0 + n.

sort Nat : Sort;
op 0 :  Nat;
op s : Nat  Nat;
op + : Nat * Nat  Nat;
axiom add_zero_left(n : Nat)      0 + n = n;
axiom add_succ_left(n m : Nat)    s(n) + m = s(n + m);
axiom refl(T : Sort, x : T)   x = x;

rule forward(T : Sort, a b : T, eq := a = b, P : T  Prop)
   P(b)
  ────────────────────────
   P(a)
end;

lemma zero_left_flip(n : Nat)
   n = 0 + n;
proof
  by wip(?goal);
wip;

Hint

Turn the 0 + n on the right into n with forward(Nat, 0 + n, n, add_zero_left(n), n = _) then n = n; and close by refl(Nat, n).

10c. Flip an equality: from a = b, prove b = a.

sort Nat : Sort;
op 0 :  Nat;
op s : Nat  Nat;
op + : Nat * Nat  Nat;
axiom add_zero_left(n : Nat)      0 + n = n;
axiom add_succ_left(n m : Nat)    s(n) + m = s(n + m);
rule symmetry(T : Sort, x y : T)
   x = y
  ────────────────────────
   y = x
end;

lemma flip_eq(a b : Nat, h := a = b)
   b = a;
proof
  by wip(?goal);
wip;

Hint

symmetry(Nat, a, b) concludes b = a from a = b: then h := a = b a = b; by h;.

That’s the toolbox. Almost every proof in the standard library — and every monster in the Dungeon Proof Crawler — is these moves, combined. When a goal stumps you, ask which shape it has (an implication? a ? an equation?) and reach for the matching technique.