3.2 Système de types avec effets de RegML
4.1.4 Le problème d’aliasing
Comme nous l’avons expliqué, les types enregistrements et les régions sont la même chose. Nous pouvons d’ailleurs reformuler le principe de Liskov en termes de régions :
Si Q(fl) est une propriété démontrable pour toute région privée fl, alors la propriété Q(flÕ) doit être vraie pour toute région flÕ telle que flÕ est un raffinement de la région fl.
Comme nous l’avons vu dans le chapitre précédent, le contrôle statique des alias fait partie des propriétés qui sont assurées par le typage de WhyML. Rappelons que cela signifie que pour toute paire de pointeurs qui apparaissent dans un programme bien typé, le système connaît statiquement si ces deux noms réfèrent à la même case mé-moire ou pas. Le principe de Liksov implique donc que si le code client était bien typé en premier lieu, il doit rester bien typé après le raffinement, y compris en ce qui concerne le contrôle statique des alias. Or, le raffinement d’une région peut y intro-duire des champs supplémentaires contenant eux-mêmes de nouvelles régions. Si l’on ne met aucune restriction sur la relation entre ces régions introduites et les régions qui existaient déjà dans le code client avant le raffinement, il devient possible de briser la barrière d’abstraction et de mettre le principe de Liskov en défaut. Par exemple, si l’ensemble des régions introduites par le raffinement n’est pas disjoint de l’ensemble des régions déjà connues du client, on peut facilement casser le typage du code client. En effet, supposons que l’on dispose de deux opérations newA () et newB () qui créent respectivement deux régions distinctes fl1 et fl2. Dans le code ci-dessous, les types et les effets seront donc :
let x = newA () : fl1· (? · fl1) in let y = newB () : fl2· (? · fl2) in
x
Maintenant, supposons que l’on raffine la région fl2 en introduisant un champ f et que fl2.f = fl1. Dans ce cas-ci, l’effet de l’appel newB () devient (? · {fl2, fl1}) où la régionfl1 devient donc invalidée. Or, la variable x est justement de type fl1 et donc ne peut pas être utilisée : le code ci-dessus devient donc rejeté par le typage, alors qu’il
était bien typé auparavant. Ainsi, on doit imposer que les régions introduites par le raffinement doivent être disjointes des régions connues auparavant.
Mais la fraîcheur des régions introduites vis-à-vis des régions existantes ne suffit pas : il y a d’autres moyens, plus subtils, d’introduire des alias inconnus du client. Considérons par exemple le code client qui définit deux ensembles mutables que l’on modifie ensuite :
let bar (x: G.node) =
let marked = MutableSet.create () in let on_stack = MutableSet.create () in
add x marked; add x on_stack;
Pour que ce code soit bien typé, il est nécessaire de supposer que les deux ensembles sont typés avec deux régions distinctes, disons fl1 etfl2. En effet, si ces régions étaient égales, la création de l’ensemble on_stack invaliderait l’utilisation de l’ensemble marked. Imaginons maintenant que l’on a choisi l’implémentation des ensembles mutables avec les tables de hachage, comme dans le module MutableSetbyHashtbl. Notons qu’a priori, rien n’empêche les régions fl1 et fl2 de partager le même tableau à l’intérieur du champ buckets. Or, même si ce tableau-là appartenait à une régionfl3 fraîche, dès lors que l’on a l’égalité fl1.data = fl2.data (où les deux expressions dénotent la même région fl3), le code de la fonction bar ci-dessus devient nécessairement mal typé. En effet, dans l’implémentation de la fonction add, la première ligne
if t.size = t.buckets.length then resize t;
a pour effet de restreindre l’utilisation de la région fl3, puisque la fonction resize est susceptible de remplacer le tableau stocké dans t.buckets par un tableau frais. La région fl3 ne sera dorénavant accessible que depuis la région fl1. Par conséquent, la dernière ligne (add x on_stack;) dans le code ci-dessus devient mal typée, car nous avons supposé que fl1 ”= fl2.
D’une manière encore plus subtile, si l’on raffine deux régions équivalentes (c’est-à-dire qui ont la même structure d’aliasing, voir la définition fl1 © fl2 dans section 3.2.1
du chapitre précédent) par deux régions qui ne le sont pas, il devient encore possible de mettre le principle de Liskov en défaut. Illustrons ce propos sur l’exemple suivant. Considérons une interface :
module GF
type t
val f: unit æ t
val g: t æ unit
end
et le code client très simple qui l’utilise :
Maintenant, supposons que l’on raffine le type t par un type enregistrement avec deux composantes mutables :
type t = { mutable a: array int;
mutable b: array int }
A priori, rien n’empêche de construire des données du type t correspondant à des régions structurellement égales mais non équivalentes, selon que les champs a et b sont aliasés ou non. Supposons donc que l’on donne aux fonctions f et g respectivement les signatures suivantes (ignorons ci-dessous les effets qui ne sont pas pertinents pour notre propos ici)
val f: unit æ {a : fla; b : fla}r val g: {a : fla; b : flb}rÕ æ unit
où l’on suppose que les régionsfla etflb sont distinctes. Or, puisque la règle du typage de l’appel (voir la section 3.2.4) impose que les régions des paramètres formels et les régions des arguments soient équivalentes, l’appel g(f()) devient mal typé, alors qu’il était accepté par le typage en premier lieu.
Il est donc nécessaire de trouver des conditions suffisantes, mais pas trop restric-tives, sur la manière dont les nouvelles régions sont introduites, pour garantir que le raffinement ne mette pas en défaut le principe de Liskov. Comme nous l’avons illustré avec les trois exemples ci-dessus, trouver ce genre de conditions n’est pas trivial. Par ailleurs, cette question peut être étudiée indépendamment des autres conditions de la validité du raffinement qui sont elles plus en rapport avec la préservation des proprié-tés logiques. La contribution de ce chapitre est de proposer de telles conditions et de montrer formellement leur validité. Concrètement, nous allons adapter le formalisme du chapitre précédent aux notions de régions privées, puis définir le raffinement des régions et des signatures de fonctions. Ensuite, nous montrerons que le raffinement ainsi défini préserve la relation du typage.
Insistons encore fois sur le fait que la préservation des notions logiques telles que les invariants de types et les contrats de fonctions doivent également être prises en compte pour montrer la validité du raffinement. Comme il s’agit de notions bien étudiées dans la littérature, nous ne les formalisons pas dans ce chapitre, en nous consacrant entièrement à la problématique des alias.