I will present the conversion procedure that is used in András Kovács' Elaboration zoo and its extension to the η-rule for the unit type. It is based on Coquand's type-checking algorithm.
In Normalization by Evaluation we distinguish between terms and values. There is an evaluation function that evaluates terms into values, and a quoting (or read-back) function that converts values back into terms.
There are multiple ways to implement quoting: it would be possible to quote values into normal forms, but it is preferable to quote values into terms that are as small as possible. In particular we don't want to do unnecessary η-expansions.
Conversion checking is performed at the level of values.
For a type theory with Type : Type
, Pi-types, Sigma-types and a unit type, the following representation of values is used (in OCaml):
type value_t
= Neutral of neutral
| Type
| Pi of value_t * closure
| Lam of value_t (* the domain type *) * closure
| Sigma of value_t * closure
| Pair of value_t * value_t
| Unit
| UnitTT
and closure = value_t -> value_t
and neutral
= NVar of int (* de Bruijn level *) * value_t (* the type of the variable *)
| NApp of neutral * value_t
| NProj1 of neutral
| NProj2 of neutral
There may be multiple representations of the same semantic value. For example Pair(UnitTT,UnitTT)
and Neutral(NVar(0, Sigma(Unit, fun _ -> Unit)))
should be convertible.
With this value representation, the conversion checking procedure needs to handle the following cases:
let rec conv lvl : value_t * value_t -> bool = function
| Type,Type -> true
| Pi(a1,b1),Pi(a2,b2) -> __
| Sigma(a1,b1),Sigma(a2,b2) -> __
| Unit,Unit -> true
| Lam(a1,b1), Lam(a2,b2) -> __
| f1, Lam(a2,b2) -> __
| Lam(a1,b1), f2 -> __
| Pair(a1,b1), Pair(a2,b2) -> __
| p1, Pair(a2,b2) -> __
| Pair(a1,b1), p2 -> __
| UnitTT,_ -> true
| _,UnitTT -> true
| Neutral(n1),Neutral(n2) ->
if is_strict_prop lvl (neutral_type n1)
then true
else conv_neutral lvl n1 n2
| _,_ -> false (* The values are not convertible. *)
The missing parts __ depend on other elements of the implementation. The corresponding code in the elaboration-zoo can be found here, but it doesn't handle the unit type.
The argument lvl
of conv
is an integer that has to be greater than any variable occurring in the value. It is used to generate a new variable when checking whether closures are convertible. Alternatively, it is possible to use fresh names.
For Pi-types and Sigma-types, checking the η-rule is done by the cases that compare a lambda or a pair with a neutral value. For the η-rule of the unit type, this is not sufficient, because for example two different variables may be convertible. However the only missing cases involves two neutral values. It suffices to check whether the type is a strict proposition when checking the conversion of two neutral values.
let rec is_strict_prop lvl : value_t -> bool = function
| Type -> false
| Unit -> true
| Pi(a,b) -> is_strict_prop (lvl+1) (b (var lvl a))
| Sigma(a,b) -> is_strict_prop lvl a && is_strict_prop (lvl+1) (b (var lvl a))
| Neutral(_) -> false
Conversion assumes that the two values have the same type.
However conversion is not type-directed: the common type of the two values is not an argument of conv
. Instead every variable is tagged with its type in the representation of values. This provides a way to compute the type of a neutral value when needed. Recomputing the type of a general value is however not possible with this representation.