In this paper we are interested in describing one of the key ingredients in the implementation of Interactive Theorem Provers (ITP) based on type theory.
The architecture of these tools is usually organized in layers and follows the so called de Bruijn principle: the correctness of the whole system solely depends on the innermost component called kernel. Nevertheless, from a user perspective, the most interesting layers are the external ones, the ones he directly interacts with. Among these, the refiner is the one in charge of giving a meaning to the terms and types he writes. The smarter the refiner is, the more freedom the user has in omitting pieces of information that can be reconstructed. The refiner is also the component generating the majority of error messages the user has to understand and react to in order to finish his proof or definition.
In this and in the previous paper  we are interested in the implementation of interactive theorem provers (ITP) for dependently typed languages that are heavily based on the Curry-Howard isomorphism. Proofs are represented using lambda-terms. Proofs in progress are represented using lambda-terms containing metavariables that are implicitly existentially quantified. Progression in the proof is represented by instantiation of metavariables with terms. Metavariables are also useful to represent missing or partial information, like untyped lambda-abstractions or instantiation of polymorphic functions to omitted type arguments.
Agda  and Matita  are examples of systems implemented in this way. Arnaud Spiwack in his Ph.D. thesis  partially describes a forthcoming release of Coq 8.4 that will be implemented on the same principles.
The software architecture of these systems is usually built in layers. The innermost layer is the kernel of the ITP. The main algorithm implemented by the kernel is the type checker, which is based in turn on conversion and reduction. The type checker takes as input a (proof) term possibly containing metavariables and it verifies if the partial term is correct so far. To allow for type-checking, metavariables are associated to sequents, grouping their types together with the context (hypotheses) available to inhabit the type. The kernel does not alter metavariables since no instantiation takes place during reduction, conversion or type checking.
The kernel has the important role of reducing the trusted code base of the ITP. Indeed, the kernel eventually verifies all proofs produced by the outer layers, detecting incorrect proofs generated by bugs in those layers. Nevertheless, the user never interacts directly with the kernel and the output of the kernel is just a boolean that is never supposed to be false when the rest of the system is bug free. The most interesting layers from the user point of view are thus the outer layers. The implementation of a kernel for a variant of the Calculus of (Co)Inductive Constructions (CIC) has been described in  down to the gory details that make the implementation efficient.
The next layer is the refiner and is the topic of this paper. The main algorithm implemented by the refiner is the refinement algorithm that tries to infer as much information as it is needed to make its input meaningful. In other words it takes as input a partial term, written in an “external syntax”, and tries to obtain a “corresponding” well typed term. The input term can either be user provided or it can be a partial proof term generated by some proof command (called tactic) or automation procedure. The gap between the external and internal syntax is rather arbitrary and system dependent. Typical examples of external syntaxes allow for:
Untyped abstractions. Hence the refiner must perform type inference to recover the explicit types given to bound variables. The polymorphism of CIC is such that binders are required to be typed to make type checking decidable.
Omission of arguments, in particular omission of types used to instantiate polymorphic functions. Hence the refiner must recover the missing information during type inference to turn implicit into explicit polymorphism.
Linear placeholders for missing terms that are not supposed to be discovered during type inference. For instance, a placeholder may be inserted by a tactic to represent a new proof obligation. Hence the refiner must turn the placeholder into a metavariable by constraining the set of free variables that may occur in it and the expected type.
Implicit ad-hoc sub-typing determined by user provided cast functions (called coercions) between types or type families. Hence the refiner must modify the user provided term by explicitly inserting the casts in order to let the kernel completely ignore sub-typing. Coercions are user provided functions and are thus free to completely ignore their input. Thus a refiner that handles coercions is actually able to arbitrarily patch wrong user provided terms turning them into arbitrarily different but well typed terms. Moreover, the insertion of a coercion between type families can also introduce new metavariables (the family indexes) that play the role of proof obligations for pre-conditions of the coercion. For instance, a coercion from lists to ordered lists can open a proof obligation that requires the list to be sorted.
The refiner is the most critical system component from the user point of view since it is responsible for the “intelligence” of the ITP: the more powerful the refiner is, the less information is required from the user and the simpler the outer layers become. For instance, a series of recent techniques that really improve the user experience have all been focused in the direction of making the refiner component more powerful and extensible by the user. Canonical structures , unification hints  and type classes  are devices that let the user drive some form of proof search that is seamlessly integrated in the refinement process. While the latter device is directly integrated into the refinement algorithm, the first two are found in the unification algorithm used by the refiner.
They all make it possible to achieve similar objectives, the second being more general than the first and the last two being incomparable from the point of view of efficiency (where the second is best) and expressiveness (where the third is more flexible). The implementation of type classes done in Coq is actually provided by an additional layer outside the refiner for historical reasons.
In this paper we will describe only the refinement algorithm implemented in a refiner for a variant of the Calculus of (Co)Inductive Constructions. The algorithm is used in the forthcoming major release of the Matita111Matita is free software available at http://matita.cs.unibo.it ITP (1.0.x). The algorithm calls a unification algorithm that will be specified in this paper and described elsewhere. We do not consider type classes in our refinement algorithm since we prefer to assume the unification algorithm to implement unification hints. Nevertheless, type classes can be easily added to our algorithm with minor modifications and indeed the relevant bits that go into the refiner are implemented in Matita.
Before addressing bi-directionality, which is a peculiarity of the algorithm that has not been fully exploited yet222The refinement algorithm of Coq 8.3, the most widespread implementation of CIC, is almost mono-directional with only the lambda-abstraction case handled in a bi-directional way. Many other interesting cases of bi-directionality are obtained in this paper for inductive types and constructors. for the CIC, we just conclude our overview of an ITP architecture by talking about the next layer. The next layer after the refiner is that of tactics. This layer is responsible for implementing commands that help the user in producing valid proof terms by completely hiding to him the proof terms themselves. Tactics range from simple ones that capture the introduction and elimination rules of the connectives (called primitive tactics) to complicated proof automation procedures. The complexity of proof automation is inherent in the problem. On the other hand, primitive tactics should be as simple as building small partial proof terms. For instance, to reduce a proof of to a proof of given it is sufficient to instantiate the metavariable associated to the sequent with the term in external syntax where is a placeholder for a new proof obligation. This is possible when the refinement algorithm is powerful enough to refine to where is a new metavariable associated to the sequent . When this is not the case or when the refiner component is totally missing, the tactic is forced to first perform an analysis of the current goal, then explicitly create a new metavariable and its sequent, and then emit the new proof term directly in the internal syntax.
When the external syntax of our ITP allows to omit types in binders, the refinement algorithm must perform type inference. Type inference was originally studied in the context of lambda-calculi typed a la Curry, where no type information can be attached at all to the binders. The traditional algorithm for type inference, now called uni-directional, performs type inference by first traversing the term in a top-down way. When a binder is met, a new metavariable (usually called type or unification variable in this context) is introduced for the type of the bound variable. Then type constraints are solved traversing the term in a bottom-up way. When the variable or, more generally, a term is used in a given context, its type (called inferred type) is constrained to be compatible with the one expected by the context (called expected type). This triggers a unification problem.
Type inference, especially for the Hindley-Milner type system, gives the possibility to write extremely concise programs by omitting all types. Moreover, it often detects a higher degree of polymorphism than the one expected by the user. Unluckily, it has some drawbacks. A minor one is that types are useful for program documentation and thus the user desires to add types at least to top level functions. In practice, this is always allowed by concrete implementations. Another problem is error reporting: a typing error always manifests itself as a mismatch between an inferred and an expected type. Nevertheless, an error can be propagated to a very distant point in the code before being detected and the position where it is produced. The mismatch itself can be non informative about where the error actually is. Finally, unification quickly becomes undecidable when the expressive power of the type system increases. In particular, it is undecidable for higher order logic and for dependent types.
To avoid or mitigate the drawbacks of type inference, bi-directional type-checking algorithms have been introduced in the literature . These algorithms take as input a -term typed a la Curry and an expected top-level type and they proceed in a top-down manner by propagating the expected type towards the leaves of the term. Additional expected types are given in local definitions, so that all functions are explicitly documented. Error detection is improved by making it more local. The need for unification is reduced and, for simple type systems, unification is totally avoided. Some terms, in particular -redexes, are no longer accepted, but equivalent terms are (e.g. by using a local definition for the head). An alternative consists of accepting all terms by re-introducing a dependency over some form of unification.
Bi-directionality also makes sense for languages typed à la Church, like the one we consider here. In this case the motivations are slightly different. First of all, typing information is provided both in the binders and at the top-level, in the form of an expected type. Hence information can flow in both direction and, sooner or later, the need to compare the expected and inferred types arises. In the presence of implicit polymorphism, unification is thus unavoidable. Because of dependent types and metavariables for proof obligations, we need the full power of higher order unification. Moreover, again because of unification, the problem remains undecidable also via using a bi-directional algorithm. Hence avoiding unification is no longer a motivation for bi-directionality. The remaining motivations for designing a bi-directional refinement algorithm for CIC are the following:
Improved error messages.
A typing error is issued every time a mismatch is found between the inferred and expected type. With a mono-directional algorithm, the mismatch is always found at the end, when the typing information reaches the expected type. In a bi-directional setting the expected type is propagated towards the leaves and the inferred type towards the root, the mismatch is localized in smaller sub-terms and the error message is simpler. For instance, instead of the message “the provided function has type but it is supposed to have type ” related to a whole function definition one could get the simpler message “the list element has type but it is supposed to have type ” related to one particular position in the function body.
Improvement of the unification algorithm.
To make the system responsive, the semi-decidable unification algorithm is restricted to always give an answer in a finite amount of time. Hence the algorithm could fail to find a solution even when a solution exists. For instance, the algorithms implemented in Coq and Matita are essentially backtracking free and they systematically favor projections over mimics: when unifying an applied metavariable with a (for some closed in a context ), the system instantiates with rather than (where ). Moreover, unification for CIC does not admit a most general unifier and it should recursively enumerate the set of solutions. However, it is usual in system implementations to let unification return just one solution and to avoid back-tracking in the refinement algorithm333To the authors knowledge, Isabelle  is the only interactive prover implementing Huet’s algorithm  capable of generating all second order unifiers. Thus, if the solution found by unification is correct locally, but not globally, refinement will fail. Thanks to bi-directionality, unification problems often become more instantiated and thus simpler, and they also admit fewer solutions. In particular, in the presence of dependent types, it is easy to find practical examples where the unification algorithm finds a solution only on the problems triggered by the bi-directional algorithm.
An interesting and practical example that motivated our investigation of bi-directionality is the following. Consider a dependently typed data-type that represents the syntax of a programming language with binders. Type dependency is exploited to make explicit the set of variables bound in the term and every variable occurrence must come with a proof that the variable occurs in the bound variables list: has type where is a variable name, I is a proof of True and Var has type where is a computable function that reduces to True when belongs to and to False otherwise. Consider now the term in concrete syntax that represents in our programming language. Note that no information about the set of bound variables has been provided by the user. Thus it is possible to simply define notational macros so that the user actually writes and this is expanded444User provided notational macros are used to extend the external syntax of an ITP and they are expanded before refinement, yielding a term in external syntax to be refined. to . A uni-directional refiner is unlikely to accept the given term since it should guess the right value for the second placeholder such that reduces to True and is the set of variables actually bound in the term. The latter information is not local and it is still unknown in the bottom-up, uni-directional approach. On the other hand, a bi-directional refiner that tries to assign type to the term would simply propagate to the first placeholder and then propagate to the second one, since Lambda, which is a binder, has type . Finally, True is the inferred type for I, whose expected type is . The two types are convertible and the input is now accepted without any guessing.
Improvement of the coercion mechanism.
Coercions are triggered when unification fails. They are explicit cast functions, declared by the user, used to fix the type of sub-terms. Simplifying the unification problem allows to retrieve more coercions. For instance, consider a list of natural numbers used as a list of integer numbers and assume the existence of a coercion function from natural to integers. In the mono-directional problem, the failing unification problem is vs . The coercion required is the one obtained lifting over lists. The lifting has to be performed manually by the user or by the system. In the latter case, the system needs to recognize that lists are containers and has to have code to lift coercions over containers, like in . In the bi-directional case, however, the expected type would propagate to assign to each list element the expected type and the coercion would be applied to all integers in the list without need of additional machinery. The bi-directional algorithm presented in this paper does not allow to remove the need for the coercion over lists in all situations, but it is sufficient in many practical ones, like the one just considered.
Introduction of vectors of placeholders (“…”) in the external syntax.
A very common use of dependently typed functions consists in explicitly passing to them an argument which is not the first one and have the system infer the previous arguments using type dependencies. For instance, if and is a list of integers, the user can simply write and have the system infer that must be instantiated with the type of , which is .
This scenario is so common that many ITPs allow to mark some function arguments as implicit arguments and let the user systematically avoid passing them. This requires additional machinery implemented in the ITP and it has the unfortunate drawback that sometimes the user needs to explicitly pass the implicit arguments anyway, in particular in case of partial function applications. This special situation requires further ad-hoc syntax to turn the implicit argument into an explicit one. For instance, if we declare the first argument of Cons implicit, then the user can simply write for the term presented above, but has to write something like , in Coq syntax, to pass the partial function application to some higher order function expecting an argument of type .
An alternative to implicit arguments is to let the user explicitly insert the correct number of placeholders “?” to be inferred by the system. Series of placeholders are neither aesthetic nor robust to changes in the type of the function.
A similar case occurs during the implementation of tactics. Given a lemma , to apply it the tactic opens new proof obligations by refining the term where the number of inserted placeholders must be exactly .
In this paper we propose a new construct to be added to the external syntax of ITPs: a vector of placeholders to be denoted by and to be used in argument position only. In the actual external syntax of Matita we use the evocative symbol “…” in place of . The semantics associated to is lazy: an will be expanded to the sequence of placeholders of minimal length that makes the application refineable, so that its inferred type matches its expected type. In a uni-directional setting no expected type is known in advance and the implementation of the lazy semantics would require computationally expensive non-local backtracking, which is not necessary in the bi-directional case.
Thanks to vectors of placeholders the analysis phase of many primitive tactics implementation that was aimed at producing terms with the correct number of placeholders can now be totally omitted. Moreover, according to our experience, vectors of placeholders enable to avoid the implementation of implicit arguments: it is sufficient for the user to insert manually or by means of a notation a before the arguments explicitly passed, with the benefit that the automatically adapts to the case of partial function application. For example, using the infix notation for , the user can both write , which is expanded to and refined to , and pass to an higher order function expecting an argument of type . In the latter case, is expanded to that is refined to because of the expected type. If is passed instead to a function expecting an argument of type , then will be expanded simply to Cons whose inferred type is already the expected one.
The rest of the paper explains the bi-directional refinement algorithm implemented in Matita . The algorithm is presented in a declarative programming style by means of deduction rules. Many of the rules are syntax directed and thus mutually exclusive. The implementation given for Matita in the functional OCaml language takes advantage of the latter observation to speed up the algorithm. We will clarify in the text what rules are mutually exclusive and what rules are to be tried in sequence in case of failure.
The refinement algorithm is presented progressively and in a modular way. In Section 3 we introduce the mono-directional type inference algorithm for CIC implemented following the kernel type checker code (that coincides with type inference if the term is ground) of Matita. The presentation is already adapted to be extended in Section 4 to bi-directional refinement. In these two sections the external and internal syntaxes coincides. In Section 5 we augment the external syntax with placeholders and vectors of placeholders. Finally, in Section 6 we add support for coercions. In all sections we will prove the correctness of the refinement algorithms by showing that a term in external syntax accepted by the refiner is turned into a new term that is accepted by the kernel and that has the expected type. Moreover, a precise correspondence is established between the input and output term to grant that the refined term corresponds to the input one.
We begin introducing the syntax for CIC terms and objects in Table 1 and some naming conventions.
To denote constants we shall use …; the special case of (co)recursively defined constants will be also denoted using …; we reserve …for variables; …for terms; …for types and we use …for sorts.
We denote by a context made of variables declarations () or typed definitions (). We denote the capture avoiding substitution of a variable for a term by . The notation is for simultaneous parallel substitution.
To refer to (possibly empty) sequences of entities of the same nature, we use an arrow notation (e.g. ). For the sake of conciseness, it is sometimes convenient to make the length of a sequence explicit, while still referring to it with a single name: we write to mean that is a sequence of exactly elements and, in particular, that it is a shorthand for ; the index must be a natural number (therefore the notation refers to a non-empty sequence). The arrow notation is extended to telescopes as in or
and used in binders, (co)recursive definitions and pattern matching branches.
As usual, is abbreviated to when is not a free variable in . Applications are n-ary, consisting of a term applied to a non-empty sequence of terms.
Inductive types are annotated with the number of arguments that are homogeneous in the types of all constructors. For example consider the inductive type of vectors Vect of arity . It takes two arguments, a type and a natural number representing the length of the vector. In the types of the two constructors, and , every occurrence of Vect is applied to the same argument , that is also implicitly abstracted in the types of the constructors. Thus Vect has one homogeneous argument, and will be represented by the object
and referred to with . This is relevant for the pattern matching construction, since the homogeneous arguments are not bound in the patterns because they are inferred from the type of the matched term. For example, to pattern match over a vector of type the user writes
The inductive type in the pattern matching constructor is (almost) redundant, since distinct inductive types have distinct constructors; it is given for the sake of readability and to distinguish the inductive types with no constructors. In a concrete implementation it also allows to totally drop the names of the constructors by fixing an order over them: the -th pattern will be performed on the -th constructor of the inductive type.
Since inductive types may have non homogeneous arguments, not every branch is required to have exactly the same type. The term introduced with the return keyword is a function that computes the type expected by a particular branch and also the type of the entire pattern matching. Variables are abstracted in the right hand side terms of .
The definitions of constants (including (co)recursive constants ), inductive types and constructors are collected in the syntactic category of CIC objects .
Metavariable occurrences, represented with , are missing typed terms equipped with an explicit local substitution. The index enables metavariables to occur non-linearly in the term. To give an intuition of the role played by the local substitution, the reader can think of as a call to the, still unknown, function with actual arguments . The terms will be substituted for the formal arguments of the function inside its body only when it will be known.
We omit to write the local substitution when it is the identity substitution that sends all variables in the current context with themselves. Thus will be a shorthand for when are the variables bound in the right order in the context of the metavariable occurrence.
2.2. Typing rules
The kernel of Matita is able to handle the whole syntax presented in the previous section, metavariables included. While we report in the Appendix 8 the full set of typing rules implemented by the kernel, here we summarise only the ones that will be reused by the refinement algorithm. We will give a less formal but more intuitive presentation of these rules, defining them with a more concise syntax. Moreover, we will put our definition in linear order, while most of them are actually mutually recursive.
Definition (Proof problem ).
A proof problem is a finite list of typing declarations of the form .
A proof problem, as well as a CIC term, can refer to constants, that usually live in an environment that decorates every typing rule (as in the Appendix 8). In the following presentation we consider a global well formed environment , basically a collections of CIC objects defining all constants and inductive types and associating them to their respective types. No refinement rule will modify this environment that plays no role in this presentation. In fact it is the task of the kernel to enable well typed definitions, inductive types and (co-)recursive functions to enter the environment.
We thus omit the environment from the input of every judgment. We will fetch from it the type of a constant, inductive type or constructor writing .
We regard CIC as a Pure Type System , and we denote by the set of axioms. We denote by any sort of the PTS, with the fact that types , and with the fact that a product over to has sort . CIC is a full but not functional PTS: all products are well formed but in it may be . This is because the calculus is parameterized over a predicative hierarchy for in a given set of universe indexes. In a predicative setting, given and , is defined as according to some bounded partial order on the universe indexes. The details for the actual PTS used in Matita are given in . We will often write simply Type when we are not interested in the universe index (e.g. in examples). We also write for the biggest sort in the hierarchy, if any, or a variable universe to be later fixed to be big enough to satisfy all the required constraints.
We also write to check if an element of an inductive type of sort can be eliminated to inhabit a type whose sort is . This is relevant for CIC since the sort of propositions, Prop, is non informative and cannot be eliminated to inhabit a data type of sort for any (but for few exceptions described in  Section 6).
Proof problems do not only declare missing proofs (i.e. not all have sort Prop) but also missing terms and, of particular interest for this paper, missing types.
Definition (Metavariable substitution environment ).
A metavariable substitution environment (called simply substitution when not ambiguous) is a list of judgments of the form
stating that the term of type in has been assigned to .
We now anticipate the typing judgment of the kernel. A formal definition of well formedness for and will follow.
Definition (Typing judgment).
Given a term , a proof problem and a substitution , all assumed to be well formed, we write
to state that is well typed of type .
When the type is well typed and its type is either a metavariable or a sort .
The typing judgment implemented in our kernel is an extension of the regular typing judgment for CIC [35, 23, 13]. It is described in  and reported in the Appendix 8. Here we recall the main differences:
Substitution of a regular variable for a term is extended with the following rule for metavariables:
The conversion relation (denoted by ) is enlarged allowing reduction to be performed inside explicit substitution for metavariables:
The following typing rules for metavariables are added:
Moreover, in many situations a metavariable occurrence is also accepted as a valid sort, marking it so that it cannot be instantiated with anything different from a sort. This additional labelling will be omitted, being marginal for the refinement algorithm.
The technical judgment states that a metavariable occurrence is well formed in .
In all the previous rules we assumed access to a global well formed proof problem and substitution . Both and are never modified by the judgments implemented in the kernel.
We now present the well formedness conditions, corresponding to the judgments presented in the Appendix 8.
Definition (Metavariables of term/context ()).
Given a term , is the set of metavariables occurring in . Given a context , is the set of metavariables occurring in .
The function is at the base of the order relation defined between metavariables.
Definition (Metavariables order relation ()).
Let be a proof problem. Let be the relation defined as: iff . Let be the transitive closure of .
Definition (Valid proof problem).
A proof problem is a valid proof problem if and only if is a strict partial order (or, equivalently, if and only if is an irreflexive relation).
The intuition behind is that the smallest (or one of them since there may be more than one) does not depend on any other metavariable (e.g. and where ). Thus instantiating every minimal with a metavariable free term will give a new in which there is at least one not depending on any other metavariable (or is empty). This definition is the key to avoid circularity in the following definitions.
In the rules given in Appendix 8 the partial order is left implicit by presenting as an ordered list. However, as proved by Strecker in his Ph.D. thesis , the order is not preserved by unification and thus in any realistic implementation is to be implemented as a set and the fact that remains a partial order must be preserved as an invariant.
Definition (Well formed context ()).
Given a well formed proof problem , a context is well formed (denoted by ) if and for every
Definition (Well formed proof problem ).
A valid proof problem is a well-formed proof problem (denoted by ) if an only if for all we have and .
Definition (Well formed substitution ()).
Given a well formed proof problem , a substitution is well formed (denoted by ) if for every we have .
The well formedness definitions given so far are actually implemented by the kernel in a more precise but less intuitive way. We thus refer to the kernel judgments in the following definition, that will be used in the specification of all refinement rules.
Definition (Well formed status ()).
Given a proof problem , a substitution and a context , the triple is well formed (denoted by ) when and and .
We shall sometimes omit , considering it equal to a default, well formed context, like the empty one. The recursive operation of applying a substitution to a term is denoted by and acts as the identity for any term but metavariables contained in , on which it behaves as follows:
Note that, thanks to the extensions to the type checking rules made in Definition 2.2, substitution application is type preserving. Substitutions do apply also to well formed proof problems in the following way:
The substitution application operation is seldom used explicitly, since all judgments take as input and give back a substitution. Nevertheless it will be used in the examples.
Definition (Weak-head normalization ()).
Given a context , substitution and proof problem , all assumed to be well formed, it computes the weak head normal form of a well typed term according to the reduction rules of CIC. It is denoted by:
Note that is in weak head normal form iff .
By abuse of notation we will write to mean that for all and . Such repeated use of weak head computation to produce spines of dependent products occur frequently in the kernel and in the refinement rules, especially when dealing with inductive types.
Definition (Conversion ()).
Given a proof problem , substitution and context , all assumed to be well formed, and two terms and , it verifies if and have a common normal form according to the rules of CIC given in Appendix 8. It is denoted by:
3. Mono-directional refinement
We now present the mono-directional refinement algorithm for CIC implemented in the old versions of Matita (0.5.x) and directly inspired by the rules for type checking implemented in the kernel. In this section we assume the external syntax to coincide with the syntax of terms. Hence the algorithm actually performs just type inference. Nevertheless, we already organize the judgments in such a way that the latter extension to bi-directionality will be achieved just by adding new typing rules.
To specify what is a refinement algorithm we must first introduce the notion of proof problem refinement. Intuitively, a pair (proof problem, substitution) is refined by another pair when the second is obtained by reducing some proof obligations to new ones. It thus represents an advancement in the proof discovery process.
Definition (Proof problem refinement ).
We say that refines (denoted by ) when and for every either or where and .
Specification (Refiner in type inference mode ()).
A refiner algorithm in type inference mode takes as input a proof problem, substitution and context, all assumed to be well formed, and a term . It fails or gives in output a new proof problem, a new substitution, a term and a type . It is denoted by:
Postcondition (parametric in ):
The specification is parametric in the relation that establishes a correspondence between the term to be refined and the refiner output . In order to prove correctness, we are only interested in admissible relations defined as follows.
Definition (Admissible relations ()).
A partial order relation is admissible when for every term in external syntax and and in internal syntax and for every variable occurring free only linearly in we have that implies .
Admissibility for equivalence relations correspond to asking the equivalence relation to be a congruence.
When the external syntax corresponds to the term syntax and coercions are not considered, we can provide an implementation that satisfies the specification by picking the identity for the relation. Combined with , the two postconditions imply that must be obtained from simply by instantiating some metavariables. In Sections 5 and 6, we shall use weaker definitions of than the identity, allowing replacement of (vectors of) placeholders with (vectors of) terms and the insertion of coercions as results of the refinement process. All the relations considered in the paper will be large partial orders over terms of the external syntax (that always include the internal syntax).
We will now proceed in presenting an implementation of a refinement algorithm in type inference mode . The implementation is directly inspired by the type checking rules used in the kernel. However, since refinement deals with terms containing flexible parts, conversion tests need to be replaced with unification tests. In a higher order and dependently typed calculus like CIC, unification is in the general case undecidable. What is usually implemented in interactive theorem provers is an essentially fist order unification algorithm, handling only some simple higher order cases. The unification algorithm implemented in Matita goes beyond the scope of this paper, the interested reader can find more details in [26, 5]. Here we just specify the expected behavior of the unification algorithm.
Specification (Unification ()).
An unification algorithm takes as input a proof problem, a substitution and a context, all assumed to be well formed, and two well typed terms and . It fails or gives in output a new proof problem and substitution. It is denoted using the following notation where can either be or be omitted. In the former case universe cumulativity (a form of sub-typing) is not taken in account by unification.
3.2.1. Additional judgments
For the sake of clarity we prefer to keep the same structure for the mono and bi-directional refiners. We thus give the definition of some functions that are trivial in the mono-directional case, but will be replaced by more complex ones in the following sections.
Even if we presented the syntax of CIC using the same category terms, types and sorts, some primitive constructors (like the and abstractions) expect some arguments to be types or sorts, and not terms. A type level enforcing algorithm forces a term in external syntax to be refined to a valid type.
Specification (Type level enforcing ()).
A type level enforcing algorithm takes as input a proof problem ,
a substitution and a context , all assumed to be well formed,
and a term . It fails or it returns a new term , a sort ,
a new substitution and proof problem .
It is denoted by:
Postcondition (parametric in ):
Note that one may want to accept a metavariable as the sort , eventually labelling it in such a way that the unification algorithm will refuse to instantiate it with a different term. The choice must be consistent with the one taken in the implementation of the kernel.
The task of checking if a term has the right type is called refinement in type forcing mode and it will be denoted by . In the mono-directional case, will be simply implemented calling the algorithm that will handle coercions in Section 6
but which, at the moment, only verifies that no coercion is needed by calling the unification procedure.
Specification (Explicit cast ()).
A cast algorithm takes as input a proof problem , a substitution
and a context , all assumed to be well formed, and a term
with its inferred type and expected type .
It fails or it returns a new term of type , a new
proof problem and substitution .
It is denoted by:
Postcondition (parametric in ):
Specification (Refiner in type forcing mode ()).
A refiner algorithm in type forcing mode
takes as input a proof problem , a substitution
and a context , all assumed to be well formed, and a term
together with its expected well formed type .
It fails or returns a term
of type , a new
proof problem and substitution .
It is denoted by:
Postcondition (parametric in ):
3.2.2. Notational conventions
The arguments and will be taken as input and returned as output in all rules that define the refiner algorithm. To increase legibility we adopt the following notation, letting and be implicit. Each rule of the form
has to be interpreted as:
Moreover we will apply this convention also to rules not returning or as if they were returning the or taken as input.
Note that the and returned by all rules considered in this paper are well formed and are also a proof problem refinement of the and provided as input. Being a proof problem refinement is clearly a transitive relation. Thus we have for free that all the omitted pairs (proof problem, substitution) are refinements of the initial ones.
3.2.3. Role of the relations and their interaction
In this paragraph we shortly present the role played by the relations , , and introduced so far and the auxiliary ones and that will be specified when needed.
The relation links a term with its inferred type, while links a term with the type expected by its context. will thus exploit the extra piece of information not only checking that the inferred type unifies with the expected one, but also propagating this information to its recursive calls on subterms (when possible). and will be defined in a mutually recursive way.
The relation links a term with its refinement asserting that the refinement is a type. This is relevant when typing binders like , where is required to be a type. In its simplest formulation the relation is a simple assertion, linking a type with itself. In Section 6 the refinement relation will admit to link a term that is not a type with a function applied to that turns its input into a type. For example may be a record containing a type and may link it with , where is the projection extracting the type from the record. is recursively defined in terms of
The relation links a term , its inferred type and the type expected by its context with a refinement of the term asserting that the refined term has type . In its simple formulation the relation is a simple assertion that and are the same and thus links with itself. In Section 6 the refinement relation will admit to explicitly cast . For example a natural number of type may be casted into the rationals refining it to . The relation is non recursive.
The relations and are auxiliary relations only used to ease the presentation of the and relations in the case of applications. Both auxiliary relations are thus recursively defined with and .
3.2.4. Rules for terms
We now give an implementation for the refiner in both modes and for the auxiliary judgments. The implementation is parametric on the unification algorithm, that is not described in this paper.
Note that is arbitrary, and the actual code prefers the predicative sorts over Prop. This is the only rule defined in this section to be non syntax oriented: in case of an incorrect choice of , backtracking is required. The actual algorithm implemented in Matita performs the choice of lazily to remain backtracking free555Laziness will be no longer sufficient to avoid backtracking when we will add additional rules to handle coercions in Section 6..
Note that the operation of firing a -redex must commute with the operation of applying a substitution . Consider for example the term and the substitution . If one applies the substitution first, and then reduces the redex obtains , whose type is . If one fires the redex fist, the fact that is substituted by in is recorded in the local substitution attached to the metavariable instance. Indeed and . Therefore is given the type by the rule .
We now state the correctness theorem holding for all the rules presented so far and for the few ones that will follow. The proof is partitioned in the following way: here we state the theorem, introduce the proof method we adopted and prove the theorem for the simple rules presented so far. Then we will introduce more complex rules, like the rule for application, and we will prove for each of them the correctness theorem.
The , , ,