• Aucun résultat trouvé

L’approche que nous avons présentée décrit d’une manière rigoureuse comment, à l’aide d’un système de types, le code fantôme peut être utilisé d’une manière sûre, concise et en même temps expressive. Bien que le langage de notre formalisme n’est qu’une preuve de concept, les idées qu’il véhicule et son système de types sont à la base de ce qui est réalisé dans l’outil Why3. Bien entendu, Why3 présente de nombreuses fonctionnalités à l’égard du code fantôme que nous n’avons pas abordées dans GhostML. Le but de cette section est de passer en revue certaines de ces fonctionnalités.

Polymorphisme de types. En Why3 le typage se fait grâce à un système de types

polymorphes (avec une inférence de types similaire à Damas-Milner [2]). Tant qu’une variable de type ne peut être instanciée que par un type scalaire, étendre notre forma-lisme avec le polymorphisme de types ne présente pas de difficultés majeures, puisque le statut fantôme y est une caractéristique propre aux variables et aux expressions, et non pas aux types eux-mêmes. Ainsi, on peut écrire ghost 42 mais on ne peut pas écrire «ghost Int».

Structures de données avec des champs fantômes. En plus du polymorphisme

de types, Why3 offre la possibilité de définir des types algébriques et des enregis-trements dans lesquels certains champs, qu’ils soient mutables ou pas, peuvent être fantômes. Par ailleurs, les types de données définis par l’utilisateur peuvent apparaître dans le code fantôme et dans le code réel. Ainsi, Why3 a un seul et unique type des ta-bleaux et l’on peut manipuler aussi bien des tata-bleaux fantômes que des tata-bleaux réels. Notons que lorsque l’on effectue le filtrage sur les valeurs de types algébriques, cela requiert quelques précautions : lorsque l’expression analysée contient une composante fantôme, on doit vérifier que dans chacun des motifs, toutes les variables introduites à gauche qui sont en rapport avec la composante fantôme doivent être traitées à droite comme des variables fantômes. De plus, à l’instar de la manière dont le code fantôme est propagé dans les conditionnelles où la valeur de test est fantôme, si l’expression fil-trée est fantôme, alors que le filtrage contient au moins deux motifs différents, on doit propager le code fantôme jusqu’à ce que la construction du filtrage devienne fantôme dans sa globalité.

Exceptions. En Why3, les exceptions sont toujours introduites globalement. Il serait

donc possible d’étendre GhostML avec un mécanisme d’exceptions similaire. En effet, il suffirait pour cela d’ajouter un nouvel indicateur fantôme qui tiendrait compte de l’ensemble des exceptions qu’une expression est susceptible de lever. On pourrait alors réutiliser les mêmes exceptions dans le code fantôme et le code réel, à condition que les exceptions levées par une expression fantôme n’échappent pas au contexte fantôme in fine, rattrapées quelque part « plus haut » par une construction try-with fantôme.

Une terminaison prouvable. À l’instar de GhostML, Why3 autorise l’utilisation

des fonctions récursives dans le code fantôme. Par ailleurs, les boucles fantômes sont également autorisées. Néanmoins, pour garantir la non-interférence, Why3 exige dans ces cas-là que l’on accompagne les définitions récursives et les boucles avec une clause de « variant » explicite à partir de laquelle Why3 engendre les conditions nécessaires à la preuve de terminaison. Notons que la preuve de ces conditions (par des solveurs SMT par exemple) correspond exactement à ce que nous avons modélisé dans notre formalisme par l’oracle CheckTermination(·) qui renvoie lorsque la terminaison peut être statiquement établie.

Références locales. En Why3 les références mutables peuvent être créées

locale-ment, passées en argument à une fonction ou en être le résultat. L’absence des réfé-rences locales limite sans doute l’expressivité de notre formalisme. Néanmoins, tant que l’on exclut la possibilité d’aliasing entre deux telles références, il serait possible d’étendre GhostML avec des références locales. Dans ce cas-là, plutôt que d’être une valeur booléenne, correspondrait alors à l’ensemble des références réelles modifiées qui sont libres danse. En particulier, une expression qui ne modifie que des références réelles définies localement serait considérée comme pure (pourvu qu’elle termine)4. Néanmoins, même lorsque les références mutables peuvent être locales, l’absence d’alia-sing reste une restriction assez forte : rien que passer une référence en argument d’une fonction peut éventuellement créer un alias. En Why3, où l’aliasing entre les références est possible sous certaines conditions, les alias sont détectés statiquement par un sys-tème de types appelées les régions qui sera le sujet central du chapitre suivant,

2.4.1 Structure des files mutables avec un champs fantôme

Nous avons déjà vu dans l’introduction un petit exemple de l’utilisation du code fantôme en Why3 (le programme calculant les nombres Fibonacci). En guise de conclu-sion, donnons-en un autre, en illustrant quelques-unes des fonctionnalités de Why3 plus évoluées dont on vient de discuter. Considérons l’implémentation des files mutables suivante dont le code complet est donné dans la figure 2.6. Ici, les files mutables sont réalisées par le type d’enregistrement queue suivant

type queue = {

mutable front: list;

mutable rear: list;

ghost mutable view: list; }

Les champs front et rear, contenant chacun une liste simplement chaînée immuable, sont destinés à implémenter les opérations de base push et pop d’une manière amortie (à la Baker). Le troisième champs view a pour but de donner le modèle logique (sous forme d’une liste immuable) de la file en question, ce qui explique son statut fantôme. 4. Nous avons représenté par à la fois les effets de terminaison et d’écriture mais on aurait pu séparer les deux. Avec les références locales, cela serait même inévitable.

Premièrement, en ce qui concerne le partage, on voit que, d’une part, le même type algébrique list défini par l’utilisateur est utilisé à la fois dans le code fantôme et le code réel et, d’autre part, dans la fonction pop (lines 34–52), la fonction réelle rev_append est appelée à la fois dans le code réel code (ligne 42) et le code fantôme (line 39).

Deuxièmement, sur l’exemple de la fonction push (lignes 27–30), où une variable locale v sert à recalculer le nouveau modèle logique pour ensuite mettre à jour le contenu du champ q.view, on peut voir comment fonctionne la propagation du code fantôme : malgré le fait que la variable v n’est pas déclarée comme fantôme, et le fait que la fonction append est aussi une fonction réelle, Why3 infère que v est une variable fantôme. En effet, la valeur q.view, qui est nécessairement fantôme, contamine le résultat du calcul de append, ce qui à son tour contamine l’expression let-in toute entière. La variable v devient donc fantôme. En particulier, Why3 renverrait une erreur si l’on essayait par exemple de lier v à un champ réel de la file q (ou d’une autre file). D’autre part, puisque l’expression append q.view (Cons x Nil) est fantôme, elle ne doit donc pas diverger en vertu du principe de la non-interférence. C’est pourquoi Why3 exige la preuve de la terminaison de la fonction append, ce qui est garanti ici par la présence de variant(ligne 8).

La galerie en ligne de programmes vérifiés avec Why3 contient plusieurs autres exemples d’utilisation de code fantôme5.

1 module Queue

2

3 type elt

4

5 type list = Nil | Cons elt list

6

7 let rec append (l1 l2: list) : list

8 variant { l1 }

9 = match l1 with

10 | Nil æ l2

11 | Cons x r1 æ Cons x (append r1 l2)

12 end

13

14 let rec rev_append (l1 l2: list) : list

15 variant { l1 }

16 = match l1 with

17 | Nil æ l2

18 | Cons x r1 æ rev_append r1 (Cons x l2)

19 end

20

21 type queue = {

22 mutable front: list;

23 mutable rear: list;

24 ghost mutable view: list; }

25

26 let push (x: elt) (q: queue) : unit

27 = q.rear Ω Cons x q.rear;

28 let v = append q.view (Cons x Nil) in

29 q.view Ω v

30

31 exception Empty

32

33 let pop (q: queue): elt

34 raises { Empty }

35 = match q.front with

36 | Cons x f æ

37 q.front Ω f;

38 q.view Ω append f (rev_append q.rear Nil);

39 x

40 | Nil æ

41 match rev_append q.rear Nil with

42 | Nil æ raise Empty

43 | Cons x f æ 44 q.front Ω f; 45 q.rear Ω Nil; 46 q.view Ω f; 47 x 48 end 49 end 50 end