• No results found

Characteristics of Mtac and Conclusions

2.1 “Logic” Programming

3.7 Characteristics of Mtac and Conclusions

Maintainability. Mtac shares with Lemma Overloading the benefits of typed tactic programming. There is a difference though: the dynamic nature of the νx binder makes, in a sense, Mtactics be “less typed” than overloaded lemmas—i.e., they may fail (block) in ways overloaded lemmas are not allowed.

Composability. The noalias Mtactic from Section 3.3.1 was presented to show how trivial is to compose Mtactics, in particular in comparison with Lemma Overloading.

We have not mentioned how, or if, we can compose easily Mtactics with standard Coq

lemmas. The answer is: yes, we can! For instance, the Mtactic noalias can be used in exactly the same way as the overloaded lemma from Section 2.6. We show here one of the examples shown at the end of that section, and the others are equally solvable.

Assume F as before, with type ∀x y : ptr.#(x != y). We can compose the execution of F with the negbTE lemma and solve the following goal:

Goal : if (x2== x3)&&(x1!= x2) then false else true Proof : by rewrite (negbTE (eval (F )))

Interactive Tactic Programming. As mentioned in Section 3.3.1.1, Mtactics can be built interactively.

Simplicity. The semantics of Mtac is quite simple, as shown by the multiple examples throughout this chapter, most notably the noalias Mtactic from Section 3.3.1. With Lemma Overloading, this example required several advanced features to work properly (Section 2.6). And for those corner cases where intuition is not enough, and we require a more precise description of the semantics, we can turn our attention to figures 3.13 and 3.20.

Formality. As mentioned in the previous paragraph, figures 3.13 and 3.20 provides the semantics of the language. Moreover, in sections3.4.3and3.6.7we provide the proof of soundness of the system. The only weakness in the semantics of Mtac, as with Lemma Overloading, is the dependency on the unification algorithm. As such, the formalization of such algorithm is crucial if we want to understand fully how Mtactics are run, and this is why we took the endeavor of building and describing a new unification algorithm in Chapter 4.

Performance. Being an interpreted language, Mtac has a competitive performance with, for instance, Ltac (another interpreted language). Furthermore, the addition of stateful tactics allows for a significant reduction of the complexity of Mtactics, as shown in Figure 3.22.

But we think we can do better. In the future we envision two orthogonal improvements to Mtac, which combined should greatly improve the overall performance of the language.

The first one is to extract Ocaml code from Mtactics and compile it, borrowing ideas from Claret et al. (2013). The main challenge is how to amortize the cost of dynamic compilation and linking. The second improvement is related to the unification algorithm, which currently is one of the major bottlenecks of Mtac. This thesis makes the first step

of explaining the process of unification (Chapter 4); the next step is to introduce the necessary mechanisms to improve its performance.

Extensibility. Mtac alone does not provide the tools to write extensible tactics. How-ever, it is possible to combine Mtac with overloading and build extensible tactics. The idea is dead simple, although it requires some thinking to make it work. Recall from Section 3.2 the definition of the pattern matching construct, mmatch, which takes a Coq list of patterns. If we manage to construct this list dynamically, then we can easily obtain an extensible tactic.

And how do we construct the list of patterns dynamically? Using overloading! As argued in Section2.7, overloading is perfect to add cases in an algorithm, so we can as well use it to add elements in a list—in this case the list of patterns of an Mtactic!

Figure 3.23 shows an extensible version of the search tactic from Section 3.1. It uses overloading for building the cases of the algorithm. For presentation purposes, we de-cided to use type classes (Sozeau and Oury,2008), instead of canonical structures, since the former, in this particular case, leads to shorter code, allowing us to concentrate on the relevant aspect of the algorithm we intend to highlight. Further down, and in Section5.1.2, we discuss the key differences between the two overloading mechanism in Coq. For the moment, the important point to take is that this code is just a proof of concept for extensible Mtactics.

The code starts defining the type of a case, CaseT, which takes the element we are looking for, x, the function f to make recursive calls, and the list s, and returns an element of type Patt, the type for an mmatch pattern. Next, in line 5, the class Case is defined, parametrized over a natural number and with only one field, the case, with an element of CaseT. The natural number will be used as the key to find instances, as will be discussed below in detail.

In line 7 we find the build cases Mtactic that, as its name suggests, is in charge of building the list of cases. It does so by increasing a number n (starting from 0), at each step constructing an unknown instance (meta-variable) X with type Case n. For each X it triggers the type class resolution mechanism in order to obtain an instantiation for X.

If such instance exists, then it appends the case in X to the list of already found cases l and recurses with the next number. If not, it returns the list l constructed so far.

Before explaining why this works, we should explain how canonical structures and type classes difere in two relevant aspects of overloading: (1) the triggering of instance res-olution, and (2) the selection of a particular instance. In canonical structures, as we saw in Chapter 2, the resolution process starts at unification, when a projector of an

01 Definition CaseT : =

07 Definition build cases :#(seq CaseT) :=

08 let cases : =

09 mfix f (n : nat) (l : seq CaseT) : #(seq CaseT) :=

10 X ← evar (Case n); solve typeclasses;;

11 b ← is evar X;

12 if b then ret l

13 else f (S n) (@the case X :: l) 14 in cases 0 nil.

15

16 Definition next no : #nat := l ← build cases; retS (length l).

17

18 Instance default inst : Case 0 : =

19 {| the case := λ x f s. L [l] l ⇒ raise NotFound M |}.

20

21 Instance iterate inst : Case (run next no) : = 22 {| the case := λ x f s.

unknown instance is unified with some particular value. In that case, the instance is se-lected based on the head constructor of the value. In type classes, instead, the resolution process is triggered for every unknown instance, at specific points in the Ocaml code of Coq. For instance, the refiner triggers it after refining a term, some tactics trigger it at certain points when the instances might be needed, etc. The selection of an instance is performed by unifying the type of the unknown instance with the type of the instances in the database.

In our code, we trigger the resolution process of type classes explicitly, by calling the new Mtac constructor solve typeclasses. The call is done right after the creation of the

meta-variable X with type Case n, in the hope that the resolution process will find an instance for it. The instances of the class are created with different, increasing numbers, starting from 0. The result is that, after adding m instances, the Mtactic build cases will build a list containing cases m − 1, . . . , 0, in that specific order.

To make things easier with the numbering, we defined the next no Mtactic in line 16, which simply creates the list of cases and returns its length. This is just to help providing unique numbers to instances, although, of course, it does not solve the known “diamond dependency problem”, intrinsic to type classes.

In the code, only three instances are added, in lines 18–26. First, the default instance, added to fail gracefully when the element is not found, then the case for when the element is not in the head of the list, and finally the case for when the element is in the head of the list. Compared to the code in Figure3.1, these instances corresponds to the last three instances of the search Mtactic, but in reverse order. In the code we use the notationLpM to create pattern p using the notation for patterns provided in Section3.2.

Note the use of run next no in order to dynamically obtain the number for the instance.

Finally, in lines 28–32 we have the extensible Mtactic search ext. This Mtactic builds the set of cases and creates the mfixpoint iterating and pattern matching the list, similarly to the one in Figure3.1. But unlike in the non-extensible version, here the mmatch is constructed with the list of cases built in line 29. Since the type of the cases depends on the element x, the fixpoint function f , and list s, we need to first apply each case to these values, which we do using the standard map function in line 31.

With this function we can prove, so far, that some element is in a list resulting from cons-ing elements. For instance, the first of the two examples below searches for x2 in a list resulting form concatenating list [x1, x2] with an unknown list s. Since the concatenation function iterates the list on the left, the unification process taking place in the pattern matching of the search ext Mtactic reduces the list to (x1 :: x2 :: s), allowing the Mtactic to make progress and eventually succeed to prove the statement.

Example ex app l (x1 x2 : nat) s : x2 ∈ ([x1; x2] ++ s) : = run (search ext ).

Fail Example ex app r (x1 x2 : nat) s : x1 ∈ (s ++ [x1; x2]) : = run (search ext ).

The second example is similar, although the unknown list s is now at the left of the concatenation function. Since we have not added a case for concatenation, and the list

cannot be reduced, the Mtactic fails, as noted with the Fail command. In order to make it succeed, we just need to add the case for concatenation:

Instance app inst : Case (run next no) : = {| the case := λ A x f s. L

[ l r ] l ++ r ⇒ mtry

il ← f l;

ret (in or app l r x (or introl il)) with NotFound ⇒

ir ← f r;

ret (in or app l r x (or intror ir)) end

M |}

Note that there is nothing special about the search Mtactic; we can as well use the same pattern to build any other extensible Mtactic.

We have answered the question on how to build extensible Mtactics, but we have not motivated the need for extensible Mtactics. After all, Lemma Overloading is already a method that facilitates the construction of extensible tactics. Why do we need extensible tactics in Mtac? The answer is simple: To build easy-to-use, easy-to-compose extensible functional tactics.

We conclude this chapter noting that we have succeeded in building a language for proof automation with all the characteristics enumerated in the introduction of this thesis.

An Improved Unification