• Aucun résultat trouvé

Raffinement des types enregistrements

3.2 Système de types avec effets de RegML

4.1.3 Raffinement des types enregistrements

Avant de rentrer dans les détails techniques et d’expliquer quelle est la contribution de ce chapitre vis-à-vis du raffinement des types enregistrements, esquissons l’idée en grandes lignes. En Why3 le développement modulaire est géré par un système de modules. Il n’y a pas de notion d’interface à proprement parler : la même notion de module sert à la fois pour exprimer, utiliser et implémenter l’abstraction. Un module peut contenir des définitions purement logiques tels que des prédicats inductifs ou des axiomes, ainsi que des déclarations de programme : des procédures, des types de données mutables, des exceptions, etc. Par exemple, pour implémenter l’algorithme du parcours d’un graphe1 en profondeur, on peut définir le module DFS à l’intérieur duquel le module MutableSet est utilisé pour marquer les sommets visités :

module DFS

use import Graph as G

use import MutableSet as M

let rec dfs (c: G.node) (visited: M.t) : unit =

if c ”= G.null && not M.contains c visited then begin

M.add c visited;

dfs (G.left c) visited; dfs (G.right c) visited;

end

let traverse () : unit =

let visited = M.create () in

dfs G.root visited

end

Pour représenter l’interface de la structure d’ensemble mutable des nœuds d’un graphe, on pourrait envisager de déclarer le module MutableSet de la manière suivante :

module MutableSet

use import Graph as G

type t

val create () : t

val add (x: G.node) (t: t) : unit

val contains (x: G.node) (t: t) : bool

end

1. On considère ici un type de graphes très simple avec une seule racine root et des sommets de degré au plus deux.

Bien sûr, cela ne suffit pas pour raisonner sur l’utilisation des objets du type t : rien ne dit qu’il s’agit effectivement d’une structure d’ensemble mutable. Comme nous l’avons remarqué, la mutabilité des données est exprimée en Why3 à l’aide des types enregistrements et le fait qu’il s’agit d’une structure d’ensemble peut alors être formulé à l’aide d’un champ modèle. On commence donc par importer la théorie des ensembles mathématiques de la bibliothèque standard de Why3 :

module MutableSet

use import Graph as G

use import set.Fset as S ...

On définit ensuite le type t comme un type enregistrement (ignorons pour l’instant le mot private que nous expliquerons plus tard) :

type t = private { ghost mutable repr: S.set G.node }

On peut maintenant expliciter le comportement des trois opérations create, add et contains :

val create () : t

ensures { S.is_empty result.repr }

val add (x: G.node) (t: t) : unit

writes { t }

ensures { t.repr = S.add x (old t.repr) }

val contains (x: G.node) (t: t) : bool

ensures { result ¡ S.mem x t.repr }

Notons que la spécification de l’opération add comporte une clause writes { t } qui indique que les composantes mutables de t (ici limitées au champ repr) sont susceptibles d’être modifiées par la fonction. Notons également que le champ repr est fantôme, ce qui est une façon de dire qu’il s’agit d’un champ modèle, introduit pour les besoins de la spécification. Rien n’empêche d’introduire également des composantes mutables réelles. Ainsi, on peut ajouter au type t un champ mutable size et exprimer à l’aide d’un invariant de type qu’il doit être égal au nombre d’éléments de l’ensemble repr :

type t = private { ghost mutable repr: S.set G.node

mutable size: int }

invariant { size = S.cardinal repr }

Dans la définition du type t ci-dessus, le motprivate joue un rôle important, car sa présence permet d’établir une barrière d’abstraction que le client est tenu de respecter : la création des objets d’un type privé, ainsi que la modification de leur contenu n’est possible qu’au travers des opérations fournies par le module où le type est défini. S’il en

était autrement, le code client pourrait briser la barrière d’abstraction. Par exemple, le code

let marked = { repr = Set.empty; size = 0 } in

...

n’est compatible avec aucun raffinement du type t qui introduirait un champ supplé-mentaire. De même, le code

let marked = create () in

marked.repr Ω add x marked.repr; ()

casse l’invariant size = S.cardinal repr. La mise en place des types privés permet ainsi d’exprimer l’encapsulation de l’état interne des données mutables, un ingrédient nécessaire pour mettre en place le raffinement correctement.

Maintenant que l’on a introduit DFS, le module du client, et MutableSet, le mo-dule de l’« interface » des ensembles mutables, l’étape suivante consiste à raffiner la structure d’ensembles mutables. Pour faire cela, nous pouvons créer un autre module dans lequel le type t et les opérations create, add et contains sont redéfinis avec plus de précision.

En ce qui concerne la définition du type, on doit pouvoir fournir des champs sup-plémentaires. Cela peut servir notamment pour réaliser la structure des ensembles par un type concret, par exemple des tables de hachage avec chaînage. On commence le module d’implémentation de la façon suivante :

module MutableSetByHashtbl

use import Graph as G

(* ici on importe tous les autres modules et les theories:

de la bibliotheque standard: ensembles, listes, tableaux, ... *)

function hash G.node : int

axiom hash_nonneg: ’ k: G.node. 0 Æ hash k

function bucket (k: G.node) (n: int) : int = mod (hash k) n

type t = { ghost mutable repr: F.set G.node;

mutable size: int;

mutable buckets: array (list G.node) }

invariant { size = S.cardinal repr · les elements du tableau buckets

sont exactement les elements de l’ensemble repr et sont dans les bons buckets }

...

end

Ici, le type t contient un nouveau champ mutable buckets. Notons que l’on a exprimé l’invariant de collage entre la représentation et le modèle (écrit ci-dessus en français

pour simplifier) comme un renforcement du l’invariant du type t dans l’interface. Notons aussi que l’on a enlevé le mot private. Cela indique au système qu’il s’agit effectivement d’une réalisation des ensembles mutables destinée à être assemblée avec le code client pour générer le code exécutable. Il peut sembler que l’on ait ouvert au client de MutableSet la possibilité de briser la barrière d’abstraction. Néanmoins, si l’on suppose que le code du client est assemblé tel qu’il a été écrit en premier lieu, la barrière d’abstraction reste effectivement intacte.

En ce qui concerne le raffinement des opérations create, add et contains, on doit pouvoir d’une part préciser leurs spécifications (en affaiblissant les préconditions, en renforçant les post-conditions si nécessaire) et, d’autre part, les implémenter. Les fonctions de création et d’appartenance peuvent être implémentées (car le type t n’est plus privé) de la manière suivante :

let create ()

ensures { S.is_empty result.repr }

= { repr = S.empty; size = 0; buckets = Array.make 17 Nil }

let contains (x: G.node) (t: t) : bool

ensures { result ¡ S.mem x t.repr }

= let i = bucket x t.size in

List.mem x t.buckets[i]

En ce qui concerne l’opération d’ajout, supposons que l’on a écrit une fonction resize du type t æ unit qui remplace le tableau stocké dans le champ buckets par un tableau deux fois plus grand et recopie tous les éléments de l’ancien tableau. On peut alors implémenter la fonction add de la façon suivante :

let add (x: G.node) (t: t) : unit

ensures { t.repr = S.add x (old t.repr) }

= if t.size = t.buckets.length then resize t;

let i = bucket x t.buckets.length in

t.buckets[i] Ω Cons x t.buckets[i]; t.size Ω t.size + 1;

t.repr Ω S.add x t.repr

Supposons maintenant que nous avons spécifié l’algorithme du parcours de graphe et montré sa correction. Dans ce cas, le but est d’assembler le module DFS avec le module d’implémentation MutableSetByHashtbl afin d’obtenir un code correct par construction. Comme nous l’avons expliqué, pour que le code assemblé soit correct, le système doit vérifier au préalable que la barrière d’abstraction établie par l’interface est respectée à la fois par le client et par l’implémentation. En ce qui concerne le client, cette vérification est déjà mise en place en Why3 : le système de types vérifie notamment que le client ne manipule des données d’un type privé qu’au travers des opérations fournies. Ce qui manque actuellement, c’est donc un mécanisme de vérification que le module d’implémentation est bien un raffinement valide du module d’interface, au

sens du principe de substitution de Liskov.

Une partie des vérifications à faire qui découlent de ce principe concerne exacte-ment ce que l’on vient de faire dans l’exemple du module MutableSetByHashtbl : on doit s’assurer que l’implémentation contient au moins tous les symboles des types enregistrements, des champs et des procédures déclarés dans le module MutableSet, que le sens des contrats est préservé, etc. Mais il y a aussi une partie des vérifications qui concerne le problème d’aliasing et qui vient du fait que le raffinement introduit de nouvelles composantes mutables. Pour mieux expliquer ce problème, revenons aux notions des régions et du contrôle statique des alias que nous avons introduites dans le chapitre précédent.