• Aucun résultat trouvé

2.4 Exemple

3.1.1 Objectif

Les développements de l’interprétation abstraite ont donné naissance à de nombreux domaines abstraits représentants des propriétés variées ainsi qu’à de nombreux perfectionnements de la technique d’analyse. L’analyse de programmes manipulant des structures de données quant à elle est beaucoup plus immature. Il est raisonnable de chercher à porter l’interprétation abstraite sur l’analyse des structures de données tout en préservant les progrès acquis dans l’analyse des programmes plus simples.

Une première conséquence de cet objectif est que l’analyse de structures de données devrait respecter une certaine indépendance vis à vis des méthodes de calcul du point fixe. Il faut autant que possible que le problème de l’analyse puisse, comme c’est le cas en interprétation abstraite, s’exprimer comme la résolution d’un système d’équations. Ceci permettrait en effet d’y appli-quer les différentes méthodes de résolution, que ce soit par les méthodes de calcul de point fixe basées sur le théorème de Kleene et/ou par itération de politiques. [CGG+05]

Une idée naïve. Une seconde conséquence est la nécessité de préserver au maximum toute la théorie acquise sur les domaines abstraits. Ceci amène à l’idée suivante, illustrée par la figure 3.1. Nous voudrions pouvoir analyser ces deux programmes avec le même domaine abstrait, de sorte que celui-ci ne fasse pas la différence entre l’un et l’autre. Le premier programme ne devrait présenter aucune difficulté, tandis que le second faisant appel à des accès au tableau ne pourra probablement pas être analysé. Mais ces deux programmes sont très similaires. Il suffirait alors qu’en présence du second nous le transformions en le premier avant de le donner en analyse au domaine abstrait. De manière générale, on prend un programme avec des accès mémoire quel-conques et on le transforme en un programme ne manipulant que des variables locales simples. Ainsi, on pourra utiliser n’importe quel domaine abstrait pour analyser le programme résultat.

Décrivons une telle transformation par un algorithme naïf. On remplace chaque accès à une cellule mémoire par un accès à une variable. En particulier, pour chaque élément de chaque

x=x - y y=x+y x=y - x

(a) Programme simple

A[1]=A[1] - A[2] A[2]=A[1]+A[2] A[1]=A[2] - A[1]

(b) Programme modifié

Figure3.1 :Ces deux programmes échangent respectivement les valeurs dexetyet les valeurs deA[0] etA[1].

Pouri de1à nfaire

A[i] ← 0

Figure3.2 :Ce programme initialise les cellules d’un tableau à la valeur 0.

tableau, on introduit une nouvelle variable. Puis on remplace les accès à ces cellules par des accès aux variables correspondantes.

Pour l’exemple de la figure 3.1(b), supposons que le tableauAsoit composé de dix cellules. Introduisons dix nouvelles variablesa1, . . . ,a10 pour chacune d’elles. Remplaçons ensuite les accès àA[1] par un accès àa1et les accès àA[2] par un accès àa2. On obtient aux noms près le programme de la figure 3.1(a).

Cette transformation naïve ne fonctionnera pas dans tous les cas : comment faire lorsque l’indice de l’accès au tableau n’est pas constant ? Et comment faire lorsque la taille du tableau est non constante ? Si l’indice d’accès à une cellule varie, il n’est pas possible statiquement de remplacer l’accès à cette cellule par une variable. En outre, la plupart des langages de program-mations permettent de définir des tableaux dont la taille dépend d’une variable. Si cette taille n’est pas bornée alors le nombre de variables à introduire n’est pas fini. Cet obstacle rend l’idée inapplicable pour tous les programmes utilisant des tableaux non bornés. En outre, même si la taille des tableaux est bornée, il suffit qu’elle soit grande pour être un problème. Puisque l’effi -cacité de l’analyse dépend en grande partie du nombre de variables, il n’est pas raisonnable de chercher à en introduire trop.

Une seconde approche Ne jetons pourtant pas complètement l’idée précédente. Mettons la de coté et observons un autre exemple. Considérons le programme figure 3.2 qui initialise l’ensemble desncellules d’un tableau en leur donnant la valeur 0. Quand bien même on n’aurait pas les problèmes précédemment décrits, il ne serait pas très astucieux d’associer une variable différente à chaque cellule. Chacune d’entre elles est initialisée à la même valeur. Il semble plus approprié de décrire les cellules dans leur ensemble plutôt que de le faire individuellement.

Sur ce principe, on peut faire varier notre première idée. Au lieu d’introduire autant de nou-velles variables que de cellules on ajoute une seule variable pour l’ensemble du tableau. Pour le programme figure 3.2, on ajoute par exemple une variableareprésentant le tableauA. Supposons alors qu’après analyse on obtient en sortie de programme cette valeur abstraite :

i=n∧a=0

On pourra l’interpréter de la manière suivante : ∀`,1≤`≤n⇒ i=n∧A[`]=0

On peut opérer de la même manière sur n’importe quel programme. On ajoute une nouvelle variable pour chaque tableau et on interprète les valeurs abstraites en disant que toute propriété vraie de cette variable l’est de tout élément du tableau associé.

Ces deux premières idées sont introduites dans l’analyseur statique Astree´ . [BCC+02] Problèmes de sémantique. Ainsi on résout l’un des problèmes puisqu’on peut désormais traiter des tableaux non bornés. Mais le second problème reste toujours non résolu : on ne sait

toujours pas interpréter les affectations aux cellules dans le cas général. Ce n’est toutefois pas un obstacle insurmontable. Supposons qu’après l’initialisation du tableau on ait une affectation à l’une des cellules :

A[1]←1

On a choisi d’interpréter les propriétés de notre nouvelle variableacomme les propriétés vraies de toutes les cellules. Donc on peut dire deaqu’elle est comprise entre 0 et 1 puisque c’est le cas de toutes les cellules. On obtiendrait alors après cette affectation :

i=n∧0≤a≤1

Le cas général n’est pas beaucoup plus difficile. Considérons que nous avons une propriété

ϕ vraie de toutes les cellules. En particulierϕ est vrai de toutes les cellules non touchées par l’affectation. Après l’affectation, ce qu’on peut dire de chaque cellule du tableau c’est

• ou bien qu’elle n’a pas été affectée et qu’elle vérifie toujoursϕ,

• ou bien qu’elle a été affectée et qu’elle vaut désormais la valeur affectée.

Les propriétés vraies de toutes cellules après l’affectation sont donc celles impliquées par la disjonction des propriétés vraies avant l’affectation et des propriétés vraies de la cellule affectée. La valeur abstraite obtenue peut être calculée comme une union abstraite :

A[i]←x

(ϕ)=ϕt

a←x

(ϕ)

On désigne ce type d’affectation sous le nom d’affectations faibles, [BCC+02] car elles ne font qu’affaiblir la valeur abstraite. Nous les noterons ici { :

A[i] { x

(ϕ)=ϕt

a←x

(ϕ)

Chaque affectation à une cellule doit donc être traitée comme une affectation faible à la va-riable qui la représente. En conséquence, chaque fois qu’on traite une affectation, on obtient une propriété plus faible. On perd de l’information sur les structures de données au cours de l’interprétation.

Une troisième idée. Ce dernier point a son importance. Dans l’exemple, nous avons parlé de la propriété qu’il faut découvrir mais pas de comment on la découvre. On ne fait que perdre de l’information durant l’interprétation et on ne sait rien initialement du tableau. Le domaine abs-trait ne peut produire aucune information sur notre variableaque ce soit au début du programme ou à l’intérieur de la boucle. Et pourtant on aimerait découvrir une nouvelle propriété acquise à la fin du programme : que toutes les cellules valent 0.

Pour comprendre ce problème revenons à une preuveà la maindu programme. Ici, la propriété invariante de la boucle est que les cellules de tableau d’indice compris entre 1 etiont toutes pour valeur 0. En sortant de la boucle, on a i= net donc l’invariant nous dit que toutes les cellules sont initialisées.

Notre espoir est de réussir à transposer l’invariant de boucle. On ne peut plus se contenter d’ajouter une variable symbolisant le tableau en totalité, on a besoin de pouvoir ajouter des variables pour des sous-ensembles de cellules du tableau. On gardera la même interprétation que précédemment, c’est à dire que toute propriété vraie de cette variable l’est de toute cellule de ce sous-ensemble. Pour l’exemple, voici ce que nous pouvons faire :

• ajouter une variablea1pour les cellules d’indice compris entre 1 eti−1 dont on pourra dire qu’elles ont pour contenu 0,

• ajouter une variablea2pour la cellule d’indiceidont le contenu sera modifié dans la boucle et

• ajouter une variablea3 pour les cellules d’indice compris entre i+1 etndont on ne sait rien.

Ainsi au début d’une itération on espère avoir la valeur abstraite suivante 1≤i≤n∧a1=0

qui signifierait

1≤i≤n∧ ∀`,(1≤`≤i−1)⇒A[`]=0

On est ensuite en mesure d’interpréter l’affectation du corps de la boucle. Elle ne touche que la celluleA[i] qui est l’unique cellule représentée para2. Il est donc correct de dire que toutes les cellules représentées para2ont pour contenu 0 après l’affectation. Ce qui nous donne la valeur abstraite :

1≤i≤n∧a1=a2=0 signifiant

1≤i≤n∧ ∀`,(1≤`≤i)⇒A[`]=0∧A[i]=0

A la fin de l’itération, iest incrémenté. Les sous-ensembles de cellules changent en consé-quence. La cellule associée àa2 va rejoindre les cellules associées àa1. Mais puisquea1 eta2

vérifient la même propriété, cette propriété reste vraie du nouvel ensemble de cellules associé à

a1. On ne sait toujours rien dea3et de la nouvelle cellule associée àa2, mais on a déjà assez de propriétés pour prouver l’invariant.

Des ensembles vides de cellules. Pour être un véritable invariant de boucle, il ne manque qu’une seule chose : il doit être vrai à l’entrée de la boucle. Or à l’entrée de la boucle l’ensemble des cellules d’indice compris entre 1 et i− 1 est un ensemble vide puisque i = 1. Et tous les éléments de l’ensemble vide vérifient n’importe quelle propriété en particulier l’invariant. On serait donc en mesure de prouver complètement l’invariant... pour peu qu’on s’autorise à manipuler des variables représentant des ensembles de cellules pouvant être vides.

Il y a bien une autre alternative. On pourrait décrire l’état du programme d’exemple par deux valeurs abstraites : l’une décrivant le programme quand i = 1 et n’ayant pas de variable a1, l’autre décrivant le programme quandi > 1 et ayant une variablea1 représentant un ensemble non vide de cellules. Le même principe peut être appliqué à n’importe quel programme. Mais dans le cas général, le nombre de valeurs abstraites peut devenir grand : chacun des morceaux de tableaux que l’on décide de décrire peut être vide et l’énumération des cas est exponentielle. Ce n’est pas la solution que nous choisirons ici. Nous allons nous autoriser à décrire des sous-ensembles de cellules pouvant être vides. Cela va nous forcer à adopter un formalisme un peu moins direct quoique pas nécessairement plus complexe. La solution alternative entraîne elle aussi une complexité puisqu’il faut décrire comment se combinent toutes ces valeurs abstraites. Les conséquences de ce choix seront abordées plus tard.

i ← 2

j ← 2

Tant quei<nfaire

SiA[i]<A[1]alors A[i]↔A[j] j ← j+1 i ← i+1 A[j−1]↔A[1] (a) Programme 1 j i n a1 a2 a3 > ≤ (b) Découpage du tableau

Figure3.3 :Algorithme de segmentation d’un tableau. Ce programme découpe le tableau, met-tant les éléments inférieurs àA[1] entre 2 et j−1 et les éléments supérieurs entre j

etn.

Des propriétés relationnelles. Jusqu’à présent, nous avons introduit des variables représen-tant des sous-ensembles de cellules en leur donnant comme interprétation qu’une formule vraie de l’une de ces variables le soit aussi de toutes les cellules représentées par ces variables. Mais qu’en est t-il des propriétés faisant intervenir plusieurs de ces variables ?

La figure 3.3 montre l’algorithme de segmentation utilisé dans le tri par segmentation. Cet algorithme sépare un tableau en deux par rapport à un pivot suivant que les valeurs des cellules sont supérieures ou inférieures à ce pivot. Le pivot est lui même un élément du tableau, ici la celluleA[1]. L’indice jsépare les deux autres parties : les cellules de contenu inférieur entre 2 et j−1 et les cellules de contenu supérieur entre jetn.

Pour analyser ce programme, introduisons 3 variablesa1,a2 eta3associées aux trois parties décrites plus tôt :a1représente le pivot eta2,a3les deux sous-ensembles de cellules issus de la segmentation. On espère pouvoir obtenir une valeur abstraite similaire à celle-ci

2≤i≤n∧a2 <a1≤a3

qu’on voudra certainement interpréter de la manière suivante : 2≤i≤n∧ ∀`1, `2,

2≤`1< j∧j≤`2 ≤iA[`1]<A[1]≤A[`2]

En particulier, le sens que l’on donne à la formulea2 <a3est que quel que soit le choix d’une cellule représentée para2et quel que soit le choix d’une cellule représentée para3, la première est strictement inférieure à la seconde.

Conclusion. Quel qu’en soit le but, il semble intéressant de pouvoir exprimer des proprié-tés sur des ensembles de cellules. Nous ne savons pas encore comment choisir ces sous-ensembles ni comment l’analyse du programme se déroulera une fois ces sous-sous-ensembles choi-sis. Mais la nécessité de procéder à de tels découpages semble évidente pour un certain nombre de preuves de programmes. Une preuve d’un algorithme sur une structure de donnée utilisera généralement un découpage de cette structure de donnée, qu’il soit trivial ou complexe, afin d’ex-primer des propriétés de sous-ensembles de cellules ou de relations entre des sous-ensembles de cellules.