Proving with holes¶
Here’s the secret that makes Algae pleasant to write proofs in, not just read them: you don’t have to know the whole proof before you start. You leave a hole, and the checker tells you what goes there.
by wip; admits the current goal without proving it (a block that admits
must be closed with wip instead of qed). Its more helpful cousin,
by wip(?name), admits the goal and prints a hole report: the goal, the
context in scope, and candidate tactics. Grow your proof one step at a time.
Tip
The reports below appear on the command line and inline in the editors on this page. Press Check ▶ on the holed examples to watch it happen.
Start with just a skeleton and a hole:
import nat;
import core(symmetry);
lemma zero_left_flip(n : Nat)
⊢ n = 0 + n;
proof
by wip(?goal);
wip;
Check it, and the kernel tells you where you are and where to look next:
found hole ?goal : proof
Context:
n : Nat
Goal:
n = 0 + n
Candidates:
symmetry (rule)
transitivity (rule)
symmetry turns n = 0 + n into 0 + n = n. Apply it and slide the hole
into the then continuation to see what’s left:
import nat;
import core(symmetry);
lemma zero_left_flip(n : Nat)
⊢ n = 0 + n;
proof
by symmetry(Nat, 0 + n, n)
then ⊢ 0 + n = n;
by wip(?rest);
wip;
Now the hole reports 0 + n = n, with add_zero_left (fact) right there in
the candidates — exactly the axiom that closes it. Drop it in, swap the final
wip for qed, and you’re done:
import nat;
import core(symmetry);
lemma zero_left_flip(n : Nat)
⊢ n = 0 + n;
proof
by symmetry(Nat, 0 + n, n)
then ⊢ 0 + n = n;
by add_zero_left(n);
qed;
A module with any wip — holed or not — is incomplete: the checker reports
it and the run fails, so a hole can never sneak past as a finished proof.
Candidates are a best-effort hint (local hypotheses, facts and rules whose
conclusion matches the goal, and refl for a reflexive equation), not a
promise — but they’re usually enough to find the next by.
Holes inside a tactic¶
Once you’ve picked a tactic, a ? helps you fill it in. Put ? after a
whole application to inspect it — the checker applies the tactic and hands you
the next step, ready to paste:
import nat;
import core(symmetry);
lemma zero_left_flip(n : Nat)
⊢ n = 0 + n;
proof
by symmetry(Nat, 0 + n, n)?;
wip;
Applying it leaves:
⊢ 0 + n = n
Continue with:
then ⊢ 0 + n = n;
by wip(?goal);
Or leave individual arguments as named holes ?a and let the checker solve
them straight from the goal. symmetry’s conclusion y = x must match
n = 0 + n, which pins down ?a and ?b — and even the sort ?T,
recovered by type inference:
import nat;
import core(symmetry);
lemma zero_left_flip(n : Nat)
⊢ n = 0 + n;
proof
by symmetry(Nat, ?a, ?b) then ?g;
wip;
Holes:
?a : Nat = 0 + n
?b : Nat = n
Subgoal(s):
?g : ⊢ 0 + n = n
by symmetry?; (no arguments at all) holes every parameter at once. Holes
even work in proof-argument positions: by rewrite_r(Nat, k + 0, k, ?eq, _)?;
reports ?eq : ⊢ k + 0 = k — the equation you still owe a proof of. And a hole
the goal doesn’t pin down — a genuinely free choice, like transitivity’s
middle term — is shown with its type and no value, so you know it’s yours to pick.
Inspect scales to whatever the tactic does. If it introduces an eigenvariable —
say by forall_intro(T, _ = _)? — the suggested continuation restates it, so
then x : T ⊢ x = x; comes out ready to paste. If it leaves two or more
subgoals, you get a whole cases skeleton, one case per goal with a named
hole in each, so you can drop it in and fill the branches one at a time.