This chapter runs Algorithm 5.1 by hand on a handful of small terms and relates each run to the typing rules. The examples are chosen to show the algorithm doing each of its jobs: building a derivation, catching a wrong proof, using a hypothesis, instantiating a universal statement, and computing inside a type. Every trace below can be reproduced in the kernel with the playgrounds.
To verify a proof t of a theorem T, the kernel calls Check(⋅,t,T), which calls Infer(⋅,t) and compares the result with T. The traces show the recursive calls that Infer makes, with the typing rule each call corresponds to. Expand or collapse the nodes to follow the recursion.
The identity proof
Let Γ1=P:Sort0 and Γ2=Γ1,h:P. The derivation of the opening example of Chapter 1, with the well-formedness premises omitted, is
The algorithm builds the same derivation from the bottom up. The call Infer(⋅,λ(P:Sort0).λ(h:P).h) proceeds as follows.
Infer(⋅,λ(P:Sort0).λ(h:P).h)T-Lam
InferSort(⋅,Sort0): Infer returns Sort1, so the annotation is a type T-Sort
Freshen leaves P unchanged, since the context is empty
Infer(Γ1,λ(h:P).h)T-Lam
InferSort(Γ1,P): Infer looks up P and returns Sort0T-Var
Freshen leaves h unchanged
Infer(Γ2,h) looks up h and returns PT-Var
returns Π(h:P).P
returns Π(P:Sort0).Π(h:P).P
Each call that returns a type corresponds to one node of the derivation tree, and no call uses computation. Finally, Check compares the result with the stated type Π(P:Sort0).P→P. The two differ only in the name of one bound variable, because P→P abbreviates Π(_:P).P and neither h nor _ occurs in the body. So IsDefEq succeeds and the proof is accepted.
The same run, recorded by the kernel itself. The call view matches the list above; the proof-tree view matches the derivation.
# The same proof, given as a complete term. The kernel infers its type
# and compares it with the goal.
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : P) => h
qed
A wrong proof
Suppose a user claims that t=λ(P:Sort0).λ(h:P).P proves Π(P:Sort0).P→P. The body returns the proposition P instead of the proof h. The kernel calls Check(⋅,t,Π(P:Sort0).P→P), which first calls Infer. The calls are the same as above, except that the last one, Infer(Γ2,P), returns Sort0. So Infersucceeds, with
Infer(⋅,t)=Π(P:Sort0).Π(h:P).Sort0.
The term t is well typed, but it does not prove the claimed proposition. When Check compares the two types, neither reduces further, and renaming bound variables cannot make them equal: the innermost body is Sort0 on one side and P on the other. Thus IsDefEq fails, and Check reports a type mismatch. (Chapter 11 replays this comparison step by step with the algorithm that IsDefEq uses.)
The error is found only because the user stated the proposition to be proved. A kernel that only ran Infer would accept t and silently assign it a different type. This is why goal comes first in every script, and why qed checks against it.
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : P) => P
qed
Using a hypothesis: modus ponens
The examples so far never apply a function. Consider checking
which says "if P implies Q and P holds, then Q holds". The four λs extend the context to Γ=P:Sort0,Q:Sort0,f:P→Q,p:P as before. The body fp reaches the application case of Infer:
Infer(Γ,fp)T-App
Infer(Γ,f) returns P→Q, that is, Π(_:P).Q. Whnf leaves it unchanged, and it is a ΠT-Var
Check(Γ,p,P) succeeds, since p:P is in ΓT-Var, T-Conv
returns Q[_↦p]=Q. The name _ does not occur in Q, so the substitution does nothing
If the user had written ff instead, the second step would call Check(Γ,f,P), which fails because P→Q is not P: a function cannot be fed to itself here. Try it: change f p to f f below.
goal forall (P : Sort 0), forall (Q : Sort 0), (P -> Q) -> P -> Q
exact fun (P : Sort 0) => fun (Q : Sort 0) => fun (f : P -> Q) => fun (p : P) => f p
qed
Instantiating a universal statement
In a dependent application the substitution in T-App does real work. Let id=λ(P:Sort0).λ(h:P).h be the proof from the first example, and let Γ=Q:Sort0. Then Infer(Γ,idQ):
Infer(Γ,idQ)T-App
Infer(Γ,id) returns Π(P:Sort0).Π(h:P).P, already a ΠT-Lam
Check(Γ,Q,Sort0) succeeds T-Var
returns (Π(h:P).P)[P↦Q]=Π(h:Q).Q, that is, Q→Q
Logically, this is ∀-elimination: from "every proposition implies itself" we obtain "Q implies Q". The type of the result depends on the value of the argument Q, which is what the dependent Π-type expresses and why T-App substitutes. In the explorer, id is a defined constant and Q comes from the context; the note on the T-App node records the substitution.
def id : forall (P : Sort 0), P -> P := fun (P : Sort 0) => fun (h : P) => h
Conversion
Recall Id=λ(X:Sort0).X from Chapter 3. The core system has no definitions, so IdP is shorthand for the term (λ(X:Sort0).X)P. Consider checking
t=λ(P:Sort0).λ(h:IdP).hagainstΠ(P:Sort0).P→P.
Infer first checks that the annotation IdP is a type. InferSort(Γ1,IdP) reaches the application case of Infer: the function λ(X:Sort0).X has type Π(X:Sort0).Sort0, the argument P passes Check(Γ1,P,Sort0), and the result is Sort0[X↦P]=Sort0. So IdP is a type, and a proposition. The body h then has type IdP, so
Infer(⋅,t)=Π(P:Sort0).Π(h:IdP).IdP.
Renaming bound variables alone does not turn this into Π(P:Sort0).P→P. But IsDefEq computes both sides before comparing them, and IdP→βP, so the inferred type becomes Π(P:Sort0).Π(h:P).P and the comparison succeeds as in the first example. In the typing rules, this step is a single use of T-Conv at the root of the derivation:
The algorithm never decides where to apply T-Conv. As described in Chapter 5, conversion is hidden in the call to IsDefEq, which runs only where two types are compared.
The live derivation shows where the computation happens: the annotation IdP is checked by T-App, and the inferred type still contains IdP until Check compares it with the goal.
# The annotation of h is (fun (X : Sort 0) => X) P, which computes to P.
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : (fun (X : Sort 0) => X) P) => h
qed
Exercises
Running Infer on these by hand, as in the examples above, is a good check of one's understanding of the algorithm. Each can also be tried in a playground.
Prove Π(P:Sort0).Π(Q:Sort0).P→Q→P.
Solution
One answer is λ(P:Sort0).λ(Q:Sort0).λ(p:P).λ(q:Q).p. The proof never uses q: the system allows hypotheses to go unused.
Prove that implication is transitive:
Π(P:Sort0).Π(Q:Sort0).Π(R:Sort0).(P→Q)→(Q→R)→P→R.
Solution
One answer ends in λ(f:P→Q).λ(g:Q→R).λ(p:P).g(fp); it uses the application case twice, first for fp:Q and then for g(fp):R.
goalforall (P : Sort 0), forall (Q : Sort 0), forall (R : Sort 0), (P -> Q) -> (Q -> R) -> P -> R
exactfun (P : Sort 0) => fun (Q : Sort 0) => fun (R : Sort 0) => fun (f : P -> Q) => fun (g : Q -> R) => fun (p : P) => g (f p)
qed
Explain why λ(P:Sort0).λ(h:P).hh is rejected, and at which line of Algorithm 5.1.
Solution
In the context P:Sort0,h:P, the body hh reaches the application case. Infer returns P for the function h, and Whnf(P)=P is a variable, not a Π. The algorithm fails at the line else fail "not a function": a proof of P is not something that can be applied. In Rust the message is application of non-function.
What to remember
A run of Infer mirrors a derivation tree; each returning call is one node.
Infer can succeed on a term that does not prove the stated theorem. Check against the stated type is what catches it.
T-App is where hypotheses are used (modus ponens) and where universal statements are instantiated (substitution into the body of a Π).
Computation inside types is handled entirely by IsDefEq; the algorithm never chooses where to apply T-Conv.