Ce TD met en œuvre la logique de Hoare sur des programmes Java. On commence par annoter le code source Java par des spécifications formelles ; puis un outil automatique (Why) produit un ensemble de formules logiques, appelées obligations de vérification, exprimant la correction du programme vis-à-vis de cette spécification ; enfin des outils de preuve automatique (par exemple: Alt-Ergo) sont utilisés pour montrer la validité de ces formules.
Vous pouvez éditer les fichiers Java de la manière habituelle (gedit, eclipse, emacs, etc.). Les outils de vérification, en revanche, seront lancés depuis un terminal. Commencez donc par ouvrir un terminal, et placez-vous dans un répertoire de votre choix qui va contenir les fichiers-sources Java de ce TD.
Exécutez la commande gwhy
, vous devez avoir comme retour
don't know what to do withExécutez alors la commande
why-config
. Ceci a dû créer dans votre répertoire home les fichiers cachés .whyrc et .gwhyrc.
class Min { int getMin(int t[]) { int res = t[0]; for (int i = 1; i < t.length; i++) if (t[i] < res) res = t[i]; return res; } }On commence par ajouter une annotation au debut du fichier pour indiquer que l'on ne souhaite pas (pour l'instant) vérifier l'absence de débordements arithmétiques :
//@+ CheckArithOverflow = no class Min { ...On peut alors passer à la spécification de la méthode getMin.
//@ requires t != null && t.length > 0; int getMin(int t[]) { ... }indique que la méthode getMin attend un tableau (non null) ayant au moins un élément. Notez la présence du point-virgule à la fin de l'annotation.
Une postcondition indique au contraire une propriété garantie par la méthode, une fois son exécution terminée. Elle est introduite par le mot clé ensures. Ainsi une postcondition pour getMin peut être par exemple
//@ ensures \forall integer i; 0 <= i < t.length ==> \result <= t[i]; int getMin(int t[]) { ... }Elle exprime que le résultat est plus petit que tout élément de t. Comme on le voit sur cet exemple, la valeur renvoyée par la méthode est dénotée par \result. On notera également que l'on utilise dans la quantification universelle le type integer, qui désigne ici les entiers mathématiques, et non le type int de Java.
Précondition et postcondition sont optionnelles ; lorsqu'elles sont présentes toutes les deux, elles doivent apparaître dans la même annotation, et chacune est terminée par un point-virgule. Ainsi on écrira
/*@ requires ...; ensures ...; */ int getMin(int t[]) { ... }
formule ::= expr | expr rel expr | formule ==> formule | formule <==> formule | formule && formule | formule || formule | \forall type ident ; formule rel ::= == | != | < | <= | > | >=
/*@ loop_invariant @ 1 <= i <= t.length && @ \forall integer j; 0 <= j < i ==> res <= t[j]; @*/ for (int i = 1; i < t.length; i++) ...Cet invariant exprime d'une part que i reste toujours compris entre 1 et t.length et d'autre part que res est plus petit que toutes les valeurs t[0],...,t[i-1]. Notez qu'il faut écrire i <= t.length et non i < t.length car l'invariant doit être vérifié à la fin de la dernière exécution du corps de la boucle, où i = t.length.
Pour prouver d'autre part la terminaison de cette boucle, il faut spécifier un variant, c'est-à-dire un entier naturel qui décroît à chaque tour de boucle. Ici t.length-i convient :
/*@ loop_invariant ... @ loop_variant @ t.length - i; @*/ for (int i = 1; i < t.length; i++) ...Notez que l'invariant et le variant doivent être spécifiés à l'intérieur d'un unique commentaire, et que chacun est terminé par un point-virgule.
Le source complètement annoté est donc le suivant :
//@+ CheckArithOverflow = no class Min { /*@ requires t != null && t.length > 0; @ ensures \forall integer i; 0 <= i < t.length ==> \result <= t[i]; @*/ int getMin(int t[]) { int res = t[0]; /*@ loop_invariant @ 1 <= i <= t.length && @ \forall integer j; 0 <= j < i ==> res <= t[j]; @ loop_variant @ t.length - i; @*/ for (int i = 1; i < t.length; i++) if (t[i] < res) res = t[i]; return res; } }Copiez ce programme dans un fichier Min.java.
gwhy Min.java &où Min.java est le fichier source. (Pensez à sauver auparavant votre fichier dans l'éditeur.) Une fenêtre s'ouvre, qui doit ressembler à ceci :
On lance alors les outils de preuve automatique en cliquant sur le sommet de la colonne correspondante (le prouveur Alt-Ergo est installé en salles infos). Le résultat sur chaque obligation est signalé par une icône :
Lorsqu'une obligation n'est pas prouvée, il y a trois raisons possibles à cela :
//@+ CheckArithOverflow = no class Exercice { ... }où ... sera remplacé par le code à vérifier.
static void loop1(int n) { //@ loop_variant ...; while (n > 0) n--; } static void loop2(int n) { //@ loop_variant ...; while (n < 100) n++; }en remplaçant ... par un variant adéquat.
//@ ensures \result == 0; static int loop3() { int i = 100; //@ loop_invariant ...; loop_variant ...; while (i > 0) i--; return i; }
/*@ requires t != null; @ ensures \result <==> \forall integer i; 0 <= i < t.length ==> t[i]==0; @*/ static boolean all_zeros(int t[]) { /*@ loop_invariant ...; @ loop_variant ...; @*/ for (int k = 0; k < t.length; k++) if (t[k]!=0) return false; return true; }Donner un invariant et un variant à la boucle for de manière à prouver la correction et la terminaison de ce programme.
//@+ CheckArithOverflow = no /*@ lemma mean: \forall integer x y; x <= y ==> x <= (x + y)/2 <= y; */ /*@ lemma div2: \forall integer x y; 0 <= x ==> 0 <= x/2 <= x; */ public class BinarySearch { static int binary_search(int t[], int v) { int l = 0, u = t.length - 1; while (l <= u) { int m = (l + u) / 2; if (t[m] < v) l = m + 1; else if (t[m] > v) u = m - 1; else return m; } return -1; } }Les deux lemmes mean et div2 sont là pour aider les prouveurs en ce qui concerne la division par 2 (on ne cherchera pas à faire prouver ces deux lemmes par les prouveurs).
Pour cela, donner à la méthode binary_search une précondition exprimant que le tableau t n'est pas null et à sa boucle while un invariant de boucle portant sur les indices l et u et un variant.
Donner une spécification formelle à cette méthode, c'est-à-dire une postcondition exprimant
Lancer Why pour effectuer la vérification.
Renforcer la postcondition pour exprimer la complétude de ce programme :
La postcondition renforcée contient une conjonction d'implications, que l'on prendra soin de parenthéser correctement (la conjonction a priorité vis-à-vis de l'implication). Que se passe-t-il si vous lancez un prouveur comme Alt-Ergo ?
Renforcer la précondition pour exprimer le caractère trié du tableau t :
Note : il existe plusieurs manières de spécifier qu'un tableau est trié. On préférera la forme à deux variables « pour tout i et tout j, si i <= j alors ... » (à la forme utilisant une quantification sur une seule variable), car elle produit des obligations de preuve plus simples, qui ne nécessitent pas de preuve par récurrence.
A nouveau, que se passe-t-il si vous lancez Why et Alt-Ergo ?
Renforcer l'invariant pour exprimer que :
Lancer Why pour effectuer la vérification.
//@+ CheckArithOverflow = yeset relancer le processus de vérification.
Il doit rester au final une seule obligation de preuve non prouvée. Elle correspond à un problème de dépassement éventuel de la capacité des entiers. Identifier et modifier la ligne correspondante dans le code, et relancer le processus de vérification. (Une fois cette question terminée, on pourra lire avec intérêt cet article).
class Tri { void tri(int[] t) { for (int i=0; i < t.length; i++){ int x = t[i]; for(int j=i-1; j >= 0 && t[j] > x; j--){ t[j+1] = t[j]; t[j]=x; } } } }
Donner cette même propriété comme post-condition.
Renforcer les invariants pour montrer la post-condition.
Renforcer les invariants pour montrer la post-condition.