x

Université Pierre & Marie Curie Maîtrise d'informatique

Programmation
Spécification et certification du logiciel
Notes de cours

P. Manoury

2001-02








1  Spécifications algébriques

Tout programmeur sait ce qu'est une spécification et tout programmeur a manipulé des spécifications. Celles-ci sont, en géneral, réduites àla simple expression de nom de fonctions accompagnés de leur type. Ces spécifications sont rassemblées dans des fichiers particuliers dits d'interface.

Par exemple, en faisant abstraction de certaines scories (ci-dessous le __P), on trouve, dans stdio.h les déclarations suivantes :
extern int getc __P ((FILE *));
extern int getchar __P ((void));
extern char* gets __P ((char*));
/* etc ... */
Ces trois déclarations régissent l'emploi des fonctions correpondantes et donnent une indication sur leur rôle. L'information contenue dans ces déclaration est une information de type. Par exemple, le fonction getc appliquée àun descripteur de fichier (argument de type FILE *) a pour valeur un entier (type int).

D'autres langages utilisent un langage de type différent qui s'apparente, extérieurement, aux formules du calcul propositionnel. Par exemple, on trouvera dans le fichier d'interface du module préchargé du langage O'CAML les déclarations :
val read_line : unit -> string
val read_int : unit -> int
val read_float : unit -> float
Ces trois déclarations aussi donnent trois fonctions de lecture sur l'entrée standard.



Type de données abstrait

En programmation, les fichiers d'interface sont en général organisés de façon àrassembler les informations concernant un même sujet ou un même objet. Ces unités constituent des modules.

Toujours pour rester dans le monde ML, on trouve dans la distribution d'O'CAML les fichierslist.mli, stack.mli, queue.mli qui contiennent les primitives de créations et de manipulation de structures de données linéaires usuelles : les listes, les piles, les files d'attente.

Analysons quelques éléments du fichier stack.mli :
type 'a t

val create: unit -> 'a t

val push: 'a -> 'a t -> unit

val pop: 'a t -> 'a

val clear : 'a t -> unit

val length: 'a t -> int
La première ligne que nous avons reproduite introduit le nom de type t pour les piles (Vu de l'extérieur, le nom complet est Stack.t). Dans le monde de la spécification algébrique, on utilise le terme sorte plutôt que celui de type. Le 'a de la déclaration indique que le type des piles est paramétrépar le type indéterminé (appelé 'a)de ses éléments. Cette première ligne permet d'énoncer les suivantes qui utiliseront le nom dit type introduit. Les quatre lignes suivantes donnent des fonctions privilégiées sur les piles permettant leur création ou leur destructuration. La dernière donne l'information de longueur. Ces quelques fonctions sont les seules dont nous disposions pour manipuler kes piles dans nos programmes. L'interface ne dit rien quand àla représentation des piles : le type t est déclaré sans être défini. L'interface stack.mli définit un type abstrait.

Néanmoins, avec ces seules informations, on peut écrire une expression ayant pour valeur une pile contenant les entiers 1, 2 3 et 4 : push 1 (push 2 (push 3 (push 4 (create())))). On dira aussi que l'expression dénote une valeur.

L'interface telle que nous l'avons résumée ci-dessus ne dit rien non plas quand au comportement des opérateurs déclarés en dehors de leur type. Le savoir que nous en avons vient d'ailleurs. Ici, des commentaires qui accompagnent les déclarations :
(* This module implements stacks (LIFOs), with in-place modification. *)

type 'a t
        (* The type of stacks containing elements of type ['a]. *)

val create: unit -> 'a t
        (* Return a new stack, initially empty. *)
val push: 'a -> 'a t -> unit
        (* [push x s] adds the element [x] at the top of stack [s]. *)
val pop: 'a t -> 'a
        (* [pop s] removes and returns the topmost element in stack [s],
           or raises [Empty] if the stack is empty. *)
val clear : 'a t -> unit
        (* Discard all elements from a stack. *)
val length: 'a t -> int
        (* Return the number of elements in a stack. *)
Ces commentaires sont partie intégrante de la spécification du module Stack. On les retrouvent dans la documentation qui accompagne la distribution du langage; le manuel de référence.

Ce que nous voulons faire est de donner un statut formel aux commentaires décrivant les opérateurs : remplacer l'expression en langue naturelle par une expression en langue formelle ou mathématique. Notre première approche sera celle des spécification algébrique oùles opérateurs sont décrits par des systèmes d'axiomes équationnels.



Spécifications équationnelles

Une spécification équationnelles est un ensemble d'identités de valeurs entre expressions. Par exemple, l'identité entre l'élément retourné par pop et le dernier empilés'écrit comme l'équation :

(pop (push x s)) = x

Mais cet opérateur souffre d'un grave défaut pour se préter facilement àla spécification algébrique : il fait deux choses àla fois ; il retourne l'élément en sommet de pile et le retire de la pile. La spécification algébrique se préte mal àl'expression des effets de bord, aussi considèrerons nous plutôt une description purement fonctionnelle des opérateurs. Chaque opération est unique et chaque symbole dénote une unique opération. Le symbole topdésignera l'élément en sommet de pile et popl'opération de retrait elle-même. On ne retiendra pas l'opérateur clearqui a peu de sens dans le monde fonctionnel.

Sort: stack

Uses: int, elt

Symbols:
 create :
® stack
 push : elt, stack
® stack
 top : stack
® elt
 pop : stack
® stack
 length : stack
® int
La figure ci-dessus décrit les premiers composants de la spécification des piles. Ces éléments constituent l'en-tête de nos spécifications. Il faut maintenant en donner le corps : les équations devant être satisfaites.



Axioms: " s:stack; x:elt.

 
[A1] (top (push x s)) = x

 
[A2] (pop (push x s)) = s

 
[A3] (length create) = 0

 
[A4] (length (push x s)) = 1+(length s)
Les variables servant àexprimer les axiomes sont déclarées avec leur sorte derrière le mot-clé Axioms. Chacune des équations est implicitement universellement quantifiée (i.e. si l'on était moins fainéants, il faudrait répéter " s:stack; x:elt.devant toutes les équations -- sauf la troisième qui ne contient pas de variables).

Il convient de faire quelques remarques sur cet ensemble d'équations sur les piles.

Résumé

En rassemblant les éléments déclaratifs et descriptifs de la spécification des piles, on obtient le module de spécification complet suivant :
Sort: stack

Uses: int, elt

Symbols:
 create :
® stack
 push : elt, stack
® stack
 top : stack
® elt
 pop : stack
® stack
 length : stack
® int

Axioms: " s:stack; x:elt.

 
[A1] (top (push x s)) = x

 
[A2] (pop (push x s)) = s

 
[A3] (length create) = 0

 
[A4] (length (push x s)) = 1+(length s)

Langage des spécifications algébriques (1)

On donne sous forme de BNF une première grammaire des modules de spécification.

Caractères réservés : : ® , ; ( ) " [ ]

Mots clef : Sort: Uses: Symbols: Axioms:

Autres unités lexicales : ident num inop

SpecMod ::= Header DecPart AxPart
 
Header ::= Sort: ident
  | Sort: ident Uses: IdentList
 
DecPart ::= Symbols: SigList
 
SigList ::= ident :SymSort
  | ident :SymSort SigList
 
SymSort ::= ® BasicSort
  | FunSort
 
BasicSort ::= ident
 
FunSort ::= ArgSortList ® BasicSort
 
ArgSortList ::= ArgSort
  | ArgSort ,ArgSortList
 
ArgSort ::= BasicSort
  | FunSort
 

AxPart ::= Axioms: EqList
  | Axioms: VarBinding EqList
 
VarBinding ::= " VarDecList .
 
VarDecList ::= VarDec
  | VarDec ;VarDecList
 
VarDec ::= VarList :VarSort
 
VarList ::= ident
  | ident ,VarList
 
VarSort ::= BasicSort
  | FunSort
 
EqList ::= Eq
  | Eq  EqList
 
Eq ::= [num] Term = Term
 
Term ::= ident
  | Term Term
  | (Term )
  | Term inop Term

Outre leur correction syntaxique, les termes doivent être correctement typés (i.e. << sortés >>) :

si s est une sorte, un terme de sorte s est soit un symbole seul de sorte ® s, soit une application de la forme (s s1 .. sn) si s est de sorte s1, .., sn ® s et s1 est de sorte s1, ... et sn est de sorte sn, soit une application de la forme s1 inop s2 si inop est un symbole infixe de sorte s1, s2 ® s.



Des listes

Voici la structure linéaire classique des listes. Nous en donnons quelques opérateurs puis nous verrons comment déduire d'autres égalités àpartir de celles posées en axiome.
Sort: list

Uses: elt, int

Symbols:
 nil :
® list
 cons : elt, list
® list
 length : list
® int
 hd : list
® elt
 tl : list
® list
 append : list, list
® list
 rev : list -> list
 rev_append : list, list -> list


Axioms: " xs, ys:list; x:elt

 
[A5] (length nil) = 0
 
[A6] (length (cons x xs)) = 1+(length xs)

 
[A7] (hd (cons x xs)) = x

 
[A8] (tl (cons x xs)) = xs

 
[A9] (append nil ys) = ys
 
[A10] (append (cons x xs) ys) = (cons x (append xs ys))

 
[A11] (rev nil) = nil
 
[A12] (rev (cons x xs)) = (append (rev xs) (cons x nil))

 
[A13] (rev_append nil ys) = ys
 
[A14] (rev_append (cons x xs) ys) = (rev_append xs (cons x ys))

Theorems: " xs, ys, zs:list

 
[T1] (append xs nil) = xs

 
[T2] (length (append xs ys)) = (length xs) + (length ys)

 
[T3] (append (append xs ys) zs) = (append xs (append ys zs))

 
[T4] (rev_append xs ys) = (append (rev xs) ys)

 
[T5] (rev xs) = (rev_append xs nil)
L'égalité T5 est intéressante du point de vue implantation puisqu'elle donne un moyen d'obtenir le miroir d'une liste en utilisant une récursion terminale.

La preuve de T1
est immédiate par induction sur la liste xs :
La preuve de T2
est aussi immédiate par induction sur la liste xs :
La preuve de T3
qui donne l'associativité de l'opérateur de concaténation appendest encore immédiate par induction sur xs(en exercice).

La preuve de T4
est une induction un peu plus rusée : on montre par induction sur la liste xsque
" ys:list. (rev_append xs ys) = (append (rev xs) ys)
La présence de la quantification dans la formule àdémontrer est primordiale car elle sera aussi présente dans l'hypothèse d'induction et en permettra l'utilisation.
L'égalité T5
est une conséquence de T4 et T1



Équations conditionnelles

Une équation conditionnelles est une expression de la forme : c1, .., cn Þ eoùles c1, .., cnsont des expressions booléennes et eune équation. Les expressions boolénnes sont appelées préconditions de l'équation e. On interprète ces sortes d'équation selon le sens usuel du connecteur propositionnel d'implication : si les conditions sont satisfaites (i.e. c1=true, .., cn=true) alors l'équation doit aussi l'être.

Les équations conditionnelles permettent des restreindre le domaine d'application de certaines équations. Par exemple, on peut spécificier un opérateur d'accès au ième éleément d'une liste en précisant le domaine de validité de l'indice :


Extends: list

Symbols:
 nth : list, int
® elt

Axioms: " xs:list; x:elt; i:int

 
[A15] (nth (cons x xs) 0) = x
 
[A16] (0 < i), (i £ (length xs)) Þ (nth (cons x xs) i) = (nth xs (i-1))
Remarquons que nos préconditions ont pour conséquence que la liste xsn'est pas vide (puisque qu'elle est de longueur non nulle).

On a, au passage, introduit dans nos spécifications le nouveau mot clé Extends qui indique que l'on étend une sorte (ici, list).

Les équations conditionnelles servent également àexprimer des alternatives comme dans la spécification suivante de l'opérateur de test d'appartenance :


Extends: list

Symbols:
 mem : elt, list
® bool

Axioms: " xs:list; x,y:elt

 
[A17] (mem x nil) = false

 
[A18] (mem x (cons x xs)) = true

 
[A19] (x¹y) Þ (mem y (cons x xs)) = (mem y xs)

Listes ordonnées

Nous voulons exprimer ici la propriété d'ordonnancement des éléments d'une liste. Il faut pour cela disposer d'une relation d'ordre sur les éléments de la sorte indéterminée elt. C'est ce que nous faisons en introduisant l'usage des mots clés Assume: et with.

La propriété d'ordonnancement sera spécifiée par l'opérateur booléen sorted.

Extends: list

Assume:
 (
<) : elt, elt ® bool
 (
£) : elt, elt ® bool

with: " e,e1,e2,e3:elt
 (e
< e) = false
 (e1
< e2) Þ (e2 < e1) = false
 (e1
< e2), (e2 < e3) Þ (e1 < e3) = true
 (e1
£ e2) = (not (e2 < e1))

Symbols:
 sorted : list
® bool

Axioms: " xs:list; x,y:elt

 
[A20] (sorted nil) = true
 
[A21] (sorted (cons x nil)) = true
 
[A22] (x £ y) Þ (sorted (cons x (cons y xs))) = (sorted (cons y xs))
 
[A23] (y < x) Þ (sorted (cons x (cons y xs))) = false
On énonce, sur les listes triées deux petits résultats qui serviront par la suite :

Extends: list

Theorems: " x,y:elt; ys:list.

 
[T6] (sorted (cons x (cons y ys))) Þ (x £ y) = true

 
[T7] (sorted (cons y ys)) Þ (sorted ys) = true
On a immédiatement 6. en remarquant que sa contraposée est exactement la quatrième clause de la définition de sorted.

Pour obtenir 7., on raisonne par cas sur ys :
Opérateur de tri.
On utilise maintenant le formalisme des spécifications algébriques pour décrire un algorithme de tri simple : le tri par insertion. On montrera ensuite sa correction.

Extends: list

Symbols:
 ins : elt, list
® list
 ins_sort : list
® list

Axioms: " xs:list; x,y:elt

 
[A24] (ins x nil) = (cons x nil)
 
[A25] (x £ y) Þ (ins x (cons y xs)) = (cons x (cons y xs))
 
[A26] (y < x) Þ (ins x (cons y xs)) = (cons y (ins x xs))

 
[A27] (ins_sort nil) = nil
 
[A28] (ins_sort (cons x xs)) = (ins (ins_sort xs))

Theorems: " xs:list

 
[T8] (sorted (ins_sort xs)) = true
Le théorème de correction
de l'algorithme exprime que la valeur obtenue par l'opérateur ins_sortest une liste ordonnée. Sa démonstration requiert un lemme auxiliaire qui exprime la propriété d'invariance de l'opérateur d'insertion inspour la propriété d'ordonnancement. Ce lemme s'énonce :
Extends: list

Lemma: " xs:list; x:elt

 
9. (sorted xs) Þ (sorted (ins x xs)) = true
Pour démontrer ce lemme d'invariance, on utilise un autre lemme auxiliaire correspondant àl'un des cas d'induction de la démonstration de 9. :
Extends: list

Lemma: " xs:list; x,y:elt

 
9.1 (x £ y), (sorted (cons x xs)) Þ (sorted (cons x (ins y xs))) = true
Preuve de 9.1: on montre
" x,y:elt. (x £ y), (sorted (cons x xs)) Þ (sorted (cons x (ins y xs))) = true

par induction sur la liste xs.

Langage des spécifications algébriques (2)

On résume les extensions au langage des spécifications.

SpecMod ::= ...
 
Header ::= Title Using Assuming
Title ::= Sort: ident
  | Extends: ident
 
Using ::=  
  | Uses: IdentList
 
Assuming ::=  
  | Assume: SigList with VarBinding EqList
 
  ·
·
·
 
 
Eq ::= [num] Term = Term
  | [num] PremList Þ Term = Term
 
PremList ::= Prem
  | Prem , PremList
 
Prem ::= Term
  | Term = Term
  | Term ¹ Term
 
  ·
·
·


Modélisation des vecteurs

Les spécifications algébriques permettent de spécifier des données a priori peu fonctionnelles telles que les tableaux.

En fait, on peu avoir une idée de ce que pourrait être la spécification d'une sorte vecten consultant l'interface du module Array d'Objective Caml :
Module Array: array operations

val length : 'a array -> int

      Return the length (number of elements) of the given array. 

val get: 'a array -> int -> 'a

      Array.get a n returns the element number n of array a. The first
      element has number 0. The last element has number Array.length a
      - 1. Raise Invalid_argument "Array.get" if n is outside the
      range 0 to (Array.length a - 1). You can also write a.(n)
      instead of Array.get a n.

val set: 'a array -> int -> 'a -> unit

      Array.set a n x modifies array a in place, replacing element
      number n with x. Raise Invalid_argument "Array.set" if n is
      outside the range 0 to Array.length a - 1. You can also write
      a.(n) <- x instead of Array.set a n x.

val make: int -> 'a -> 'a array
val create: int -> 'a -> 'a array

      Array.make n x returns a fresh array of length n, initialized
      with x. All the elements of this new array are initially
      physically equal to x (in the sense of the ==
      predicate). Consequently, if x is mutable, it is shared among
      all elements of the array, and modifying x through one of the
      array entries will modify all other entries at the same time.
Il s'en dégage essentiellement une opération de création, une opération d'accès, une opération de modification et une information de longueur ou cardinalité. Traduisons cela dans notre formalisme :


Sort: vect

Uses: int, elt

Symbols:
 length : vect
® int
 get : vect, int
® elt
 set : vect, int, elt
® vect
 make : int, elt
® vect

Axioms: " v:vect; e:elt; n,i,j:int

 
[A29] (length (make n e)) = n

 
[A30] (length (set v i e)) = (length v)

 
[A31] (0 £ i), (i < n) Þ (get (make n e) i) = e

 
[A32] (0 £ i), (i < (length v)) Þ (get (set v i e) i) = e

 
[A33] (0 £ i), (i < (length v)), (0 £ j), (j < (length v)), (i ¹ j)
    
Þ (get (set v i e) j) = (get v j)
On a essentiellemnt abstrait la vision de tableaux en termes de ses constructeurs : makeet set. On a restreint la pertinance de l'opération d'accès setpar des équations conditionnelles. Un vecteur, du point de vue algébrique est l'histoire de sa création et de ses modifications.

Pour alléger l'écriture, on définit l'appartenance au domaine d'indice des vecteurs :


Extends: vect

Uses: bool

Symbols:
 indom : int, vect
® bool

Axioms: " v:vect; i:int

 
[A34] (0 £ i), (i < (length v)) Þ (indom i v) = true

 
[A35] (indom i v) Þ (0 £ i) = true

 
[A36] (indom i v) Þ (i < (length v)) = true
Ces trois équations signifient que (indom i v)est égal àla conjonction (booléenne) de (0 £ i)et (i < (length v)).



Le plus petit indice de l'élément maximal d'un tableau

Pour finir donnons un dernier exemple de spécification d'algorithme : la recherche du plus petit indice de lélément maximal d'un tableau.

La spécification suivante définit complétement abstraitement ce qu'est ce plus petit indice (imax), puis donne une fonction de calcul dont on pourra montrer qu'elle calcule cette valeur (find_imax).


Extends: vect

Assume:
 (
<) : elt, elt ® bool
 (
£) : elt, elt ® bool

with: " e,e1,e2,e3:elt
 (e
< e) = false
 (e1
< e2) Þ (e2 < e1) = false
 (e1
< e2), (e2 < e3) Þ (e1 < e3) = true
 (e1
£ e2) = (not (e2 < e1))

Symbols:
 imax : vect
® int
 loop : int, int, vect
® int
 find_imax : vect
® int

Axioms: " v:vect; i,m:int; e:elt

 /*
Défintion de imax */
 
[A37] (indom (imax v) v) = true

 
[A38] (indom i v) Þ (get v i) £ (get v (imax v))

 
[A39] (indom i v), ((get v (imax v)) = (get v i)) Þ (imax v) £ i

 /*
Fonction de calcul: find_max */
 
[A40] (loop m (length v) v) = m

 
[A41] (0 £ i), (i < (length v)), (indom m v), (get(v,m) < get(v,i))
    
Þ (loop m i v) = (loop i i+1 v)

 
[A42] (0 £ i), (i < (length v)), (indom m v), (get(v,i) £ get(v,m))
    
Þ (loop m i v) = (loop m i+1 v)

 
[A43] ((length v)¹ 0) Þ (find_max v) = (loop 0 1 v)

Theorems: " v:vect

 
[T9] ((length v)¹ 0) Þ (find_max v) = (imax v)
La démonstration du théorème de correction de find_maxréclame, en fait, la correction de l'appel àloop :
" v:vect. ((length v)¹ 0) Þ (loop 0 1 v) = (imax v)
Pour obtenir cette correction, il faut observer qu'en fait, ce n'est pas loopelle-même qui est correcte, mais son emploi àpartir de bonnes valeurs. Par exemple (en utilisant la notation OCAML des tableau) (loop 1 2 [|5;4;3;2;1;0|])ne donne pas le résultat escompté. En revanche, si le premier argument contient la valeur du plus petit indice de l'élément maximal parmi les indices déjàexplorés, on obtiendra bien le résultat escomptéen poursuivant la recherche.


Pour exprimer, le lemme qui nous donnera notre théorème, nous avons besoin d'une variante renforcée de la spécification de imax : l'opérateur bimaxqui donne le plus petit indice de l'élément maximal des éléments contenus dans le tableau avant un certain indice :


Extends: vect

Symbols:
 bimax : int, vect
® int

Axioms: " v:vect; i,j,m:int

 
[A44] (0 < i) Þ (indom (bimax i v) v) = true

 
[A45] (0 £ j), (j < i) Þ (get v j) £ (get v (bimax i v))

 
[A46] (0 £ j), (j < i), ((get v (bimax i v)) = (get v j))
    
Þ (bimax i v) £ j

Lemma: " v:vect; i,m:int

 
[T10] ((length v) ¹ 0), (0 £ i), (i £ (length v))
    
Þ (loop (bimax i v) i v) = (imax v)
L'induction permettant de mener àbien cette preuve est la même que celle qui permet d'établir la terminaison de loop : la distance de l'indice iau dernier indice de vdécroît àchaque appel récursif.

Soit n=(length v), on prouve notre lemme par induction sur n-i :

Préfixe d'une liste

Définition récursive

Extends: list

Symbols:
 pref : list, list
® bool

Axioms: " x, y:elt; xs1, xs2, xs: list.

 
[A47] (pref nil xs) = true

 
[A48] (pref (cons x xs1) (cons x xs)) = (pref xs1 xs)

 
[A49] (x ¹ y) Þ (pref (cons x xs1) (cons y xs)) = false

 
[A50] (pref (cons x xs1) nil) = false

Theorems: " xs1, xs2: list.

 
[T11] 1 (pref xs1 (append xs1 xs2)) = true
Preuve de [T1]
par induction sur xs1

Préfixe et concaténation

Extends: list

Theorems: " xs1, xs: list.

 
[T2] (pref xs1 xs) Þ $ xs2:list. xs = (append xs1 xs2)
Preuve de [T2]
on montre " xs: list. (pref xs1 xs) Þ $ xs2:list. xs = (append xs1 xs2)par induction sur xs1

Calcul d'un préfixe de longueur donnée

Extends: list

Symbols:
 n_pref : nat, list
® list

Axioms: " n:nat; x:elt; xs:list.

 
[A5] (n_pref 0 xs) = nil

 
[A6] (n > 0) Þ (n_pref n (cons x xs)) = (cons x (n_pref (n-1) xs))

 
[A7] (n_pref n nil) = nil

Theorems: " n:nat; xs:list.

 
[T3] (pref (n_pref n xs) xs) = true
Preuve de [T3]
on montre " xs:list. (pref (n_pref n xs) xs) = truepar induction sur n

Tri par arbre binaire de recherche

Les arbres binaires

Sort: btree

Uses: elt

Symbols:
 Lf :
® btree
 Br : btree, elt, btree
® btree
 root : btree
® elt

Axioms: " x:elt; a1, a2:btree.

 
[A51] (root (Br a1 x a2)) = x

Les arbres binaires de recherche

Extends: btree

Uses: elt

Assumes:

 [Ordered elt]

Symbols:
 verify : (elt, elt
® bool), elt, btree ® bool
 maximize : elt, btree
® bool
 minimize : elt, btree
® bool
 abr : btree
® bool

Axioms: " r:elt, elt ® bool; x, y:elt; a1, a2:btree.

 
[A52] (verify r x Lf) = true

 
[A53] (verify r x a1), (verify r x a2), (r x y) Þ (verify r x (Br y a1 a2)

 
[A54] minimize = (verify £)

 
[A55] maximize = (verify ³)

 
[A56] (abr Lf) = true

 
[A57] (abr a1), (abr a2), (maximize x a1), (minimize x a2) Þ (abr (Br a1 x a2))

Le tri

Extends: list

Uses: elt, abr

Symbols:
 ins : elt, btree
® btree
 to-abr : list
® btree
 to-list : btree
® list
 to-list-bis : btree
® list
 abr-sort : list
® list

Axioms: " x, y:elt; xs:list; a, a1, a2, a3:btree.

 
[A58] (ins x a) = (Br Lf x Lf)

 
[A59] (x £ y) Þ (ins x (Br a1 y a2)) = (Br (ins x a1) y a2)

 
[A60] (x ³ y) Þ (ins x (Br a1 y a2)) = (Br a1 y (ins x a2))

 
[A61] (to-abr nil) = Lf

 
[A62] (to-abr (cons x xs)) = (ins x (to-abr xs))

 
[A63] (to-list Lf) = nil

 
[A64] (to-list (Br a1 x a2)) = (append (to-list a1) (cons x (to-list a2)))

 
[A65] (to-list-bis Lf) = nil

 
[A66] (to-list-bis (Br Lf x a2)) = (cons x (to-list-bis a2))

 
[A67] (to-list-bis (Br (Br a1 x a2) y a3)) = (to-list-bis (Br a1 x (Br a2 y a3)))

Theorems " x:elt; xs:list; a:btree.

 (sorted (abr-sort xs))
= true

 (abr (to-abr xs))
= true

 (abr a)
Þ (abr (ins x a))

 (abr a)
Þ (sorted (to-list a))

Graphes 1

Les couples

Sort: pair

Uses: elt1, elt2

Symbols:
 mkpair : elt1, elt2
® pair
 fst : pair
® elt1
 snd : pair
® elt2

Axioms: " x1:elt1; x2:elt2.

 
[A68] (fst (mkpair x1 x2)) = x1

 
[A69] (snd (mkpair x1 x2)) = x2

Theorems: " x:pair.

 (mkpair (fst x) (snd x))
= x

Listes d'associations

Extends: list

Uses: pair

Symbols:
 assoc : elt1, list
® elt2
 add-assoc : elt1, elt2, list
® list

Axioms: " x, x':elt1; y, y':elt2; ys, xys:list.

 
[A70] (assoc x (cons (mkpair x y) xys)) = y

 
[A71] (x ¹ x') Þ (assoc x (cons (mkpair x' y) xys)) = (assoc x xys)

 
[A72] (add-assoc x y nil) = (cons (mkpair x y) nil)

 
[A73] (add-assoc x y (cons (mkpair x ys) xys)) = (cons (mkpair x (cons y ys)) xys)

 
[A74] (x ¹ x') Þ (add-assoc x y (cons (mkpair x' ys) xys)) = (cons (mkpair x' ys) (add-assoc x y xys))

Theorems: " x:elt1; y:alt2; xys:list.

 (mem y (assoc x (add-assoc x y xys)))
= true

Liste d'adjacences

Oùl'on voit la faiblesse du typage avec paramètres implicites.

Extends: list

Uses: pair

Symbols:
 to-adj : list -> list

Axioms: "

 [A75] (to-adj nil) = nil

 
[A76] (to-adj (cons (mkpair x y) xys)) = (add-assoc x y (to-adj xys))

Theorems: " x:elt1; y:elt2; g:list.

 (mem (mkpair x y) g)
Þ (mem y (assoc x (to-adj g))) = true

 (mem y (assoc x (to-adj g)))
Þ (mem (mkpair x y) g) = true

2  Preuve de programmes impératifs

Assertions

Nous allons àprésent utiliser la spécification de l'opérateur imax (et de son alter-ego bimax) pour établir la correction d'un programme impératif calculant l'indice de l'élément maximal d'un tableau.

On peut donner en Objective Caml une version impérative de notre fonction de recherche de l'indice de l'élément maximal :
let find_max v =
 let m = ref 0 in
 let i = ref 1 in
  while !i < Array.lenth v do
   if v.(!m) < v.(!i) then m := i;
   incr i
  done;
  !m 
;;
Affirmer la correction de ce programme. c'est affirmer que la valeur retournée par (find_max v)est celle de (imax v). On peut indiquer cette égalitésous forme d'un commentaire insérédans le texte du programme :
let find_max v =
 let m = ref 0 in
 let i = ref 1 in
  while !i < Array.lenth v do
   if v.(!m) < v.(!i) then m := i;
   incr i
  done;
  !m (* !m = (imax v) *)
;;
De tels commentaires s'appellent des assertions. Une assertion affirme que la formule qu'elle contient est vraie àl'endroit du programme oùelle est insérée. Cette spacialitédes assertions vient du style impératif de programmation : un programme impératif est une suite d'instructions.

Intuitivement, ce programme est correct car la boucle whileconserve la propriétéd'invariance que mcontient toujours l'indice de l'élément maximal parmi ceux explorés. En d'autres termes, on a, tout au long du programme !m = (bimax !i v).

On peut alors préciser la raison de la correction en détaillant les assertions vérifiées àchaque étape du calcul :
let find_max v =
 let m = ref 0 in
 let i = ref 1 in 
  (* a1 : !m = (bimax !i v) *)
  while !i < Array.length v do
   (* a2 : (!i < Array.length v) & (!m = (bimax !i v)) *)
   if v.(!m) < v.(!i) then m := i;
   (* a3 : !m = (bimax !i+1 v) *)
   incr i
   (* a4 : !m = (bimax !i v) *)
  done;
  (* a5 : (!i = Array.lenth v) & (!m = (bimax !i v)) *)
  !m 
  (* a6 : !m = (imax v) *)
;;
Examinons brièvement chacune de nos assertions :

Théorie axiomatique des programmes

On peut se convaincre intuitivement que la fonction find_maxsatisfait l'ensemble des assertions qui commentent son code. Mais pourquoi se contenter de si peu lorsque l'on peut formaliser la relation entre le langage de programmation et le langage de spécification ?

Une formalisation de ce rapport est connu sur le nom de logique de Hoare. Les énoncés de cette logique sont des triplets de la forme [P] C [Q] oùP et Q sont des formules du langage de spécification et C une expression du langage de programmation. La formule P est appelée précondition et Q, postcondition.
Intuitivement, les formules P et Q expriment les contraintes et propriétés que doivent satisfairent les variables du programme C. La précondition P décrit l'état des variables avant exécution de C et Q, l'état ou, le résultat, obtenu aprés l'exécution de C. La relation entre pré- et post- condition est définie par induction sur les règles de construction du langage de programmation.

Nous considérons un tout petit noyau des langages de programmation impératifs :
Instruction vide SKIP
Affectation x := e
Séquence C1 ; C2
Conditionnelle If e then C1 else C2
Boucle While e do C

On présente les définitions sous forme de règles de déduction :
 

[P] SKIP [P]
 
 

[P[e/x]] x := e [P]
 
[P] C1 [Q]   [Q] C2 [R]

[P] C1 ; C2 [R]
 
[PÙ e=true] C1 [Q]     [PÙ e=false] C2 [Q]

[P] If e then C1 else C2 [Q]
 
[PÙ e=true] C [P]

[P] While e do C [PÙ e=false]
L'ensemble de ces règles décrivant le comportement des instruction du langage de programmation vis-à-vis des états exprimés par les formules est complétépar une dernière règle autorisant des transformations purement logiques des pré- et post- conditions :
PÞ P'   [P'] C [Q']   Q'Þ Q

[P] C [Q]
En scindant cette règle en deux, on obtient les deux règles dérivées suivantes :
PÞ P'   [P'] C [Q]

[P] C [Q]
Renforcement de la précondition
 
[P] C [Q']   Q'Þ Q

[P] C [Q]
Affaiblissement de la postcondition


find_max revisité

Reformulons dans le langage axiomatiséci-dessus la fonction find_max :
m := 0;
i := 1;
While i < (length v) do begin
 If v(m) < v(i) then m := i else SKIP;
 i := i+1
end
Nous voulons montrer que si vest un tableau non vide alors, aprés exécution de find_max, la variable mcontient la valeur de (imax v). C'est ce qu'exprime le triplet :
[(length v)¹0] find_max [m=(imax v)]
Le programme, simple, find_maxest constituéd'une initialisation (C0) suivie d'une boucle (While e do C). Nous voulons établir [P] C0; While e do C [Q]. Àce shéma de programme correspond le schéma de preuve suivant :
[P] C0 [I]    
IÙ ¬ e Þ Q    
[IÙ e] C [I]

[I] While e do C [IÙ ¬ e]

[I] While e do C [Q]

[P] C0; While e do C [Q]
Suivant ce schéma de preuve, pour établir la correction de notre programme, il nous faut donc trouver une formule I (dit invariant) tel que :
  1. [P] C0 [I]
  2. IÙ ¬ e Þ Q
  3. [IÙ e] C [I]
Preuve de correction (partielle) de find_max
Nous avons déjàvu l'invariant nécessaire ànotre preuve, il s'agit de l'égalitém = (bimax i v). Passons donc aux trois étapes définies ci-dessus :
  1. Il faut montrer que :
    [(length v)¹0] m:=0; i:=1 [m = (bimax i v)]
    Il est facile de vérifier que 0 = (bimax 1 v) est vrai. On a donc trivialement l'implication :
    (length v)¹0 Þ 0 = (bimax 1 v)
    En utilisant la règle de renforcement de la précondition, on obtient la séquence d'initialisation avec :
    [0=(bimax 1 v)] m := 0 [m = (bimax 1 v)] et [m = (bimax 1 v)] i := 1 [m = (bimax i v)]
    cqfd
  2. En utilisant les équations de bimax, on peut montrer que si i ³ (length v)alors (bimax i v) = (imax v). D'où
    m = (bimax i v) Ù ¬(i < (length v)) Þ m = (imax v)
  3. Vient maintenant le gros morceau : montrer que l'invariant est conservépar le corps de la boucle. C'est àdire (en écrivant es triplets verticalement) :
    [ m = (bimax i v) Ù i < (length v)]
    If v(m) < v(i) then m := i else SKIP;i := i+1
    [ m = (bimax i v)]
    Ou encore, par les règles de la séquence et de l'affectation :
    [ m = (bimax i v) Ù i < (length v)]
    If v(m) < v(i) then m := i else SKIP;
    [ m = (bimax i+1 v)]
    i := i+1
    [ m = (bimax i v)]
    La seconde partie de la séquence est immédiate, reste àvoir :
    [ m = (bimax i v) Ù i < (length v)]
    If v(m) < v(i) then m := i else SKIP;
    [ m = (bimax i+1 v)]
    Par la règle de l'alternative, il faut montrer que (a)
    [ m = (bimax i v) Ù i < (length v) Ù v(m) < v(i)]
    m := i
    [ m = (bimax i+1 v)]
    et que (b)
    [ m = (bimax i v) Ù i < (length v) Ù v(m) ³ v(i)]
    SKIP;
    [ m = (bimax i+1 v)]
    1. de m = (bimax i v)et v(m) < v(i), on peut déduire que i = (bimax i+1 v). Et, par règle de l'affectation, on a
      [i = (bimax i+1 v)] m := i [m = (bimax i+1 v)]
      On obtient le résulat attendu par renforcement de la précondition.
    2. de m = (bimax i v)et v(m) ³ v(i), on peut déduire que m = (bimax i+1 v). On a alors le résultat attendu par la règle du SKIPet renforcement de la précondition.
    cqfd

Raffinement

L'axiomatique de Hoare-Floyd permet de vérifier a posteriori qu'un programme donnéstatisfait une spécification exprimée en terme de préet post conditions. L'idée du raffinement est d'obtenir la correction des programmes a priori en régentant leur construction àpartir de leur spécification. La dérivation des programmes obéit àun certain nombre de règles qui permettent d'introduire petit-à-petit les constructions usuelles des langages de programmation. On élimine ainsi progressivement toutes les formulations non calculatoires des spécifications pour obtenir un code exécutable. Chaque règle est conçue de façon àgarantir la correction au sens de Hoare-Floyd (nous verrons comment ci-dessous).

La discipline du raffinement conçoit en fait les programmes comme un classe particulière de spécification. On obtient ainsi un langage mélangeant des spécifications sous forme de préet post conditions (notées [P, Q]) et des constructions des langages de programmations. Ainsi,on peut obtenir des expressions comme :
If B then [P1, Q1] else [P2, Q2]
On voit donc les structures de controle des langages de programmations comme des combinateurs de spécifications. De façon générale, on définit l'ensemble des spécifications : La relation de raffinement etre deux spécification S1 est S2 est notée :
S1Ê S2
Les combinateurs devront être monotones par rapport àla relation de raffinement : si S1Ê S'1 et S2Ê S'2 alors La monotonie permet un raffinememnt modulaire des spécifications.



Règles de raffinement

Ne rien faire (Sk)
[P, P] Ê SKIP

Affectation (As)
[Q[e/x], Q] Ê x := e

Séquence (Sq)
[P, Q] Ê [P, R] ; [R, Q]

Variable locale (Vr)
[P, Q] Ê {Var x1.. xn. [P,Q] }
x1.. xn n'apparaissent ni dans P ni dans Q.

Combinant ces trois dernières règles, on peut dériver le programme d'échange de deux valeurs comme suit :
Soient N et M deux valeurs entières et x, y deux variables
[x=NÙ y=M, x=MÙ y=N] Ê {Var z . [x=NÙ y=M, y=NÙ x=M] }
  Ê {Var z .
      [x=NÙ y=M, z=NÙ y=M] ;
      [z=NÙ y=M, y=NÙ x=M] }
  Ê {Var z .
      z := x ;
     [z=NÙ y=M, y=NÙ x=M] }
  Ê {Var z .
      z := x ;
      [z=NÙ y=M, z=NÙ x=M] ;
      [z=NÙ x=M, y=NÙ x=M] }
  Ê {Var z .
      z := x ;
      x := y ;
      [z=NÙ x=M, y=NÙ x=M] }
  Ê {Var z .
      z := x ;
      x := y ;
      y := z }
Les deux règles suivantes permettent du raffinememnt purement logique, sans contrepartie algorithmique.
Précondition (St)
[P, Q] Ê [R, Q] si PÞ R

Postcondition (Wk)
[P, Q] Ê [P, R] si RÞ Q

L'application des règles de renforcement de la précondition et de affaiblissement de la postcondition est soumise àune condition : vérifier la validitéd'une formule. On appelle de telles conditions des obligations de preuve.

Affectation bis (Ab)
Lorsque la précondition implique la postcondition modulo une substitution, on obtient, par renforcement une deuxième règle pour l'affectation :
[P, Q] Ê x := e si PÞ Q[e/x]
En effet
[P, Q] Ê [Q[e/x], Q] (St)
  Ê x := e (As)

Initialisation (Ai)
En utilisant (St), (As) et (Sq) on obtient une autre règle dérivée (Ai) permettant l'initialisation d'une variable :
[P, Q] Ê x := e ; [PÙ x=e, Q]
En effet :
[P, Q] Ê [PÙ e=e, Q] (St)
  Ê [PÙ e=e, PÙ x=e] ; (Sq)
    [PÙ x=e, Q]
  Ê x := e ; (As)
    [PÙ x=e, Q]

On complète notre jeu de règle par les structures de contrôles de base.
Conditionnelle (If)
[P, Q] Ê If B then [PÙ B, Q] else [PÙ ¬ B, Q]

Boucle (Wh)
[I, IÙ ¬ B] Ê While B do [PÙ BÙ e=n, IÙ e<n] si PÙ B Þ e ³ 0
Dans cette dernière règle, n est une valeur entière et e une expression qui soint làpour garantir la terminaison de la boucle.



Exemple

Voici comment on peut dériver une boucle calculant la division euclideinne de deux entiers. On sait que si X et Y sont deux entiers leur quotient est Q et leur reste R si l'on a : X = (Y× Q) + R. Il faut de surcroît que R £ Y et, comme précondition, que Y > 0.

[Y > 0, X = (Y× Q) + R Ù R £ Y] Ê R := X ; (Ai)
    [Y>0Ù R=X , X = (Y× Q) + R Ù R £ Y]
  Ê R := X ; Q := 0 ; (Ai)
    [Y>0Ù R=XÙ Q=0 , X = (Y× Q) + R Ù R £ Y]
  Ê R := X ; Q := 0 ;  
    [Y>0Ù X = (Y× Q) + R , X = (Y× Q) + R Ù R £ Y] (St)
  Ê R := X ; Q := 0 ;  
    [Y>0Ù X = (Y× Q) + R , X = (Y× Q) + R Ù ¬(Y £ R)] (Wk)
  Ê R := X ; Q := 0 ;  
    While (Y £ R) do (Wh)
      [Y>0Ù X = (Y× Q) + R Ù Y £ R Ù R=n,
       Y>0Ù X = (Y× Q) + R Ù R<n]
  Ê R := X ; Q := 0 ;  
    While (Y £ R) do
      [Y>0Ù X = (Y× (Q+1) + (R-Y) Ù Y £ R Ù R=n, (St)
       Y>0Ù X = (Y× Q) + R Ù R<n]
  Ê R := X ; Q := 0 ;  
    While (Y £ R) do
      [Y>0Ù X = (Y× (Q+1) + (R-Y) Ù Y £ R Ù R=n,
       Y>0Ù X = (Y× Q) + (R-Y) Ù Y £ R Ù R=n] (Sq)
      [Y>0Ù X = (Y× Q) + (R-Y) Ù Y £ R Ù R=n,
       Y>0Ù X = (Y× Q) + R Ù R<n]
  Ê R := X ; Q := 0 ;  
    While (Y £ R) do
      Q := Q+1 ; (As)
      [Y>0Ù X = (Y× Q) + (R-Y) Ù Y £ R Ù R=n,
       Y>0Ù X = (Y× Q) + R Ù R<n]
  Ê R := X ; Q := 0 ;  
    While (Y £ R) do
      Q := Q+1 ; R = R-Y (Ab)
Nous donnons ci-dessous la liste des obligations de preuves (résumées) dans l'ordre de leur apparition au cours de la dérivation :
  1. X=RÙ Q=0 Þ X=(Y× Q)+R
  2. R£ Y Þ ¬(Y£ R)
  3. X = (Y× Q) + R Þ X = (Y× (Q+1) + (R-Y)
  4. Y>0Ù X = (Y× Q) + (R-Y) Ù Y £ R Ù R=n Þ X = (Y× Q) + (R-Y) Ù R-Y < n

Hoare-Floyd et le raffinement

On peut justifier la correction des règles basiques de raffinement en terme de logique de Hoare-Floyd. Pour cela, on interprète une spécification comme l'ensemble des programmes qui la satisfont, au sens de Hoare-Floyd. Pour l'écrire, on pose :
[P, Q] = {  C  |  |-[P] C [Q]  }
où|-[P] C [Q] signifie que le triplet [P] C [Q] est dérivable en logique de Hoare.

L'interprétation passe aux combinateurs de la façon suivante : On montre alors que si S est un raffinement de [P,Q] alors pour tout programme C appartenant à(l'interprétation de) S, le triplet [P] C [Q] est dérivable en logique de Hoare. C'est àdire :
[P,Q] Ê S Þ " CÎ S. |-[P] C [Q]

On montre l'implication en raisonnant par cas sur la règle de raffinememnt appliquée :
Ne rien faire
on a directement [P] SKIP [P].

Affectation
on a aussi [Q[e/x]] x := e [Q].

Séquence
soit C1 ; C2Î [P,R] ; [R,Q], on a par définition, C1Î [P,R] et C2Î [R,Q]. D'où, |-[P] C1 [R] et |-[R] C2 [Q], et donc |-[P] C1 ; C2 [Q].

Variable
soit Var x. CÎ Var x. [P,Q], on a, par définition CÎ [P,Q], et donc |-[P] C [Q], par définition. On a donc |-[P] Var x.C [Q], puisque x n'apparaiît pas dans P ni dans Q.

Conditionnelle
soit If B then C1 else C2Î If B then [PÙ B, Q] else [PÙ ¬ B, Q]. On a, par définition que C1Î [PÙ B, Q] et C2Î[PÙ ¬ B,Q]. D'où|-[PÙ B] C1 [Q] et |-[PÙ ¬ B] C2 [Q]. Et donc |-[P] If B then C1 else C2 [Q].

Boucle
soit While B do CÎ While B do [PÙ BÙ n=e, PÙ e<n]. On a CÎ [PÙ BÙ n=e, PÙ e<n], c'est-à-dire |-[PÙ BÙ n=e] C [PÙ e<n]. D'où|-[P] While B do C [PÙ ¬ B].

Miroir d'une liste en impératif

On utilise la spécification des listes donnée page X.

On donne le code impératif avec assertions d'un programme calculant la fonction rev. Les variables sont xs, ys:listet L est une constante donnant la valeur initiale de xs.
(* xs = L *)
ys := nil;
(* (append (rev xs) ys) = (rev L) *)
While xs
¹ nil do
 
(* xs ¹ nil Ù (append (rev xs) ys) = (rev L) *)
  ys := (cons (hd xs) ys);
 
(* xs ¹ nil Ù (append (rev (tl xs)) ys) = (rev L) *)
  xs := (tl xs)
 
(* (append (rev xs) ys) = (rev L) *)
done.
(* ys = (rev L) *)

Preuve par Hoare-Floyd

On veut
[ xs = L ]
ys := nil;
While xs
¹ nil do
  ys := (cons (hd xs) ys);
  xs := (tl xs)
done.
[ ys = (rev L) ]
On a
xs = L
Þ (rev xs) = (rev L)
Þ (append (rev xs) nil) = (rev L) (par [T1] )
On veut donc (par aff. précondition)
[ (append (rev xs) nil) = (rev L) ]
ys := nil;
While xs
¹ nil do
  ys := (cons (hd xs) ys);
  xs := (tl xs)
done.
[ ys = (rev L) ]
On a
[ (append (rev xs) nil) = (rev L) ]
ys := nil
[ (append (rev xs) ys) = (rev L) ]
Reste donc àmontrer
[ (append (rev xs) ys) = (rev L) ]
While xs
¹ nil do
  ys := (cons (hd xs) ys);
  xs := (tl xs)
done.
[ ys = (rev L) ]
On a
xs = nil Ù (append (rev xs) ys) = (rev L)
Þ ys = (rev L)
Il faut donc montrer
[ (append (rev xs) ys) = (rev L) ]
While xs
¹ nil do
  ys := (cons (hd xs) ys);
  xs := (tl xs)
done.
[ xs = nil Ù (append (rev xs) ys) = (rev L) ]
C'est-à-dire (règle du While)
[ xs ¹ nil Ù (append (rev xs) ys) = (rev L) ]
ys := (cons (hd xs) ys);
xs := (tl xs)
[ (append (rev xs) ys) = (rev L) ]
Or, on a On a donc que
xs ¹ nil Ù (append (rev xs) ys) = (rev L)
Þ xs ¹ nil Ù (append (tl xs) (cons (hd xs) ys)) = (rev L)
Par aff. précodition, il nous faut donc montrer
[ xs ¹ nil Ù (append (tl xs) (cons (hd xs) ys)) = (rev L) ]
ys := (cons (hd xs) ys);
xs := (tl xs)
[ (append (rev xs) ys) = (rev L) ]
On a, bien entendu
[ xs ¹ nil Ù (append (tl xs) (cons (hd xs) ys)) = (rev L) ]
ys := (cons (hd xs) ys)
[ xs ¹ nil Ù (append (tl xs) ys) = (rev L) ]
Reste donc àvoir que
[ xs ¹ nil Ù (append (tl xs) ys) = (rev L) ]
xs := (tl xs)
[ (append (rev xs) ys) = (rev L) ]
Ce qui est tout vu en oubliant le xs ¹ nilde la précondition.



Raffinement

La spécification originale de notre programme est le couple de précondition et postcondition :
[ xs = L, ys = (rev L) ]
Par aff. précondition (cf OP1):
[ (rev xs) = (rev L), ys = (rev L) ]
puis (cf OP2)
[ (append (rev xs) nil) = (rev L), ys = (rev L) ]
Par séquence:
[ (append (rev xs) nil) = (rev L), (append (rev xs) ys) = (rev L) ] ;
[ (append (rev xs) ys) = (rev L), ys = (rev L) ]
Par affectation:
ys := nil;
[ (append (rev xs) ys) = (rev L), ys = (rev L) ]
Par renf. postcondition (cf OP3):
ys := nil;
[ (append (rev xs) ys) = (rev L), xs = nil Ù (append (rev xs) ys) = (rev L) ]
Par boucle:
ys := nil;
While xs
¹ nil do
 
[ xs ¹ nil Ù (append (rev xs) ys) = (rev L), (append (rev xs) ys) = (rev L) ]
done.
Par aff. précond (cf OP4):
ys := nil;
While xs
¹ nil do
 
[ xs ¹ nil Ù (append (rev (tl xs)) (cons (hd xs) ys)) = (rev L),
   (append (rev xs) ys)
= (rev L) ]
done.
Par séquence et affectation:
ys := nil;
While xs
¹ nil do
  ys := (cons (hd xs) ys);
 
[ xs ¹ nil Ù (append (rev (tl xs)) ys) = (rev L),
   (append (rev xs) ys)
= (rev L) ]
done.
Par séquence (et aff. précond.)
ys := nil;
While xs
¹ nil do
  ys := (cons (hd xs) ys);
  xs := (tl xs)
done.
Résumé
des obligations de preuves

OP1: xs = L Þ (rev xs) = (rev L)
OP2: (rev xs) = (rev L) Þ (rev xs) = (append (rev xs) nil)
OP3: xs = nil Ù (append (rev xs) ys) = (rev L) Þ ys = (rev L)
OP4: xs ¹ nil Ù (append (rev xs) ys) = (rev L)
  Þ (append (rev (tl xs)) (cons (hd xs) ys)) = (rev L)


3  Spécification ensembliste

Une alternative àla formalisation en termes de spécification équationnelle est l'utilisation de la richesse et de la généralitéde la théorie des ensembles.

Il existe des présentations axiomatiques minimalistes de la théorie des ensembles. Néanmoins, nous pourrons nous contenter ici d'une présentation intuitive, une théorie naïve.



Langage ensembliste

Le langage ensembliste est àla fois riche et extrèment pauvre. Il est pauvre car on peut se contenter, pour développer la théorie des ensembles, du langage de la logique pure (connecteurs, quantificateurs) et du seul symbole Î. Il est riche car sur cette seule base, on peut introduire toutes les notions nécessaires aux mathématiques avec leur notation.



Relations entre ensembles

Appartenance
La relation de base entre ensembles est la relation d'appartenance notée
xÎ y
pour << x appartient ày >> ou << x est une élément de y >>. En théorie des ensembles, la notion d'élément n'a pas d'existence en soi : tout est ensemble et on est toujours élément de quelque chose.


ex Comme elle est d'un usage fréquent, on se donne la notation Ï comme une abréviation servant àdésigner la négation de la relation d'appartenance. On pose :
x Ïy º ¬(x Î y)
º représente l'égalitépar définition.


ex Dans le cadre de la théorie des ensembles utilise souvent les tournures : pour tous x appartenant ày ; encore de il existe x appartenant ày tel que ... Ces deux locutions donnent lieu aux deux abréviations suivantes :
" xÎ y. j º " x. (xÎ y Þ j )
$ xÎ y. j º $ x. (xÎ y Ù j )
phi est une formule quelconque.

Inclusion
Sur la base de la relation d'appartenance, on définit la relation d'inclusion entre x et y :
xÍ y º " z. (zÎ x Þ zÎ y)
On dit que x est sous-ensemble de y.

Égalité
Attention, il ne faut pas confondre cette égalitépar définition avec l'égalitécomme relation entre ensemble. Cette dernière se note avec le symbole usuel = dont on contraint l'usage par l'axiome :
x = y Û (" z. zÎ x Û zÎ y)
Et d'autres termes, deux ensembles sont égaux si et seulement si ils possèdent exactement les mêmes éléments. Cet axiome est appeléaxiome d'extensionalité.

Notez que l'on a le théorème :
x = y Û (xÍ y Ù y Í x)


Constructeurs d'ensembles

On introduit maintenant comment obtenir de nouveaux ensembles par combinaison. Pour chacune de ces constructions, on introduira une notation ainsi que la contrainte axiomatique en régissant l'usage. Dans la plupart des cas, il s'agira d'une contrainte portant sur les conditions d'appartenance àl'ensemble construit.

Ensemble vide
On note Ø l'ensemble vide. On pose :
" x. xÏØ

Union
On note xÈ y l'union des éléments de x et y. On pose :
" z. (zÎ xÈ y Û (zÎ x Ú zÎ y))

Notez que l'on a (théorèmes)
(x È Ø) = (Ø È x) = x

Intersection
On note xÇ y l'instersection des éléments de x et de y. On pose :
" z. (zÎ xÇ y Û (zÎ x Ù zÎ y))

Notez que l'on a (théorèmes)
(x Ç Ø) = (Ø Ç x) = Ø

Différence ensembliste
On note x\y l'ensemble x privédes éléments de y. On pose :
" z. (zÎ x\y Û zÎ x Ù zÏy)

Montrez que l'on a
x\x = Ø
x\(xÇ y) = x\y
xÍ y Þ x\y = Ø

Ensemble des parties
On note IP(x) l'ensemble de tous les sous-ensembles de x. On pose :
" z. (zÎ IPx Û zÍ x)


Couples et produit cartésien

Àpartir de deux ensembles x et y, on forme le couple (ou paire ordonnée) de ces deux ensemble que l'on note (x,y). Un couple est caractérisépar l'égalitéplutôt que par l'appartenance comme cela a étéle cas pour les constructeurs précédents. Si x, y, x' et y' sont des ensembles, on pose donc :
(x,y) = (x',y') Û x=x' Ù y=y'

Le produit cartésien de deux ensembles x et y, que l'on note x× y, est l'ensemble de tous les couples formés d'un élément de x suivi d'un élément de y. On pose :
" z. (zÎ x× y Û $ aÎ x. $ in y. z=(a,b))


Schéma de compréhension

Le schéma de compréhension exprime le concept originel d'ensemble : un ensemble est une entitéréunissant des entités possédant une propriétécommune. En logique, posséder une propriétécommune, s'exprime àl'aide d'une formule caractérisant la propriétépartagée par les éléments visés.

Pour des raisons théoriques profondes (le paradoxe de Russel), cette façon intuitive se donner des ensembles a dûêtre sérieusement contrôlée : on ne peut construire un ensemble d'éléments que comme sous-ensemble d'un ensemble déjàdonné. Si j exprime la propriétédésirée, on note {yÎx  |  j } le sous ensemble des éléments de x qui satisfont j . On pose :
" z. (zÎ {yÎx  |  j }Û zÎ x Ù j [z/y])


Relations binaires et fonctions

Une relation binaire entre un ensemble X et un ensemble Y associe des éléments de X avec des élément de Y. Cette association peut être representée comme un couple (x,y) oùxÎ X et yÎ Y (i.e. (x,y)Î X× Y). On peut alors définir l'ensemble des relations binaires entre deux ensembles X et Y :
X« Y º IP(X× Y)

On peut ainsi parler d'une relation R entre (éléments de) X et Y comme un élément de X« Y et noter RÎ X« Y. Si deux éléments x et y sont dans la relation R, on se donne la notation infixe usuelle en posant :
x 
 
R

 y º (x,y)Î R

Rappelons ce que sont les domaine (ensemble de départ) et codomaine (ensemble d'arrivée) d'une relation :
dom(R) º {xÎX  |  $ yÎ Y. x  
 
R

  y }
ran(R) º {yÎY  |  $ xÎ X. x  
 
R

  y }

Fonctions
Les fonctions sont une catégorie particulière de relations binaires : celles qui associe au plus un élément du codomaine au élément de leur domaine. Selon l'usage, nous allons employer l'opérateur logique d'existence et d'unicité$! que l'on définit par :
$! x. j º $ x. (j Ù " y. (j [y/x] Þ y=x))

Comme pour les relations (puisque c'en sont), on pourra aussi parler de l'ensemble des fonctions entre X et Y en distinguant les fonctions partielles (notées X¬® Y) des fonction totales (notées X® Y). Voici comment on définit ces deux ensembles :
X¬® Y º
{fÎX« Y  |  " xÎdom(f).$! yÎ Y. x  
 
f

  y }
X® Y º {fÎX¬® Y  |  dom(f) = X }

Si f est une fonction, on se donne la notation usuelle (du résultat) de l'application en posant l'axiome :
f(x)=y Û (x,y)Î f
Ce ne peut être une simple définition, car l'expression f(x)=y suppose l'existence de y.

Opérations sur les relations
On définit, sur les relations binaires un certain nombre d'opérateurs utiles dans le cadre de la spécification. Cette dernière opération est massivement utilisée lorsque l'on veut modifier en un point la valeur d'une fonction. En notant x|® y le couple (x,y), on a :
ì
í
î
fÅ{x|® y }(x) = y
fÅ{x|® y }(z) = f(z) si z¹x


Arithmétique

On se donne l'arithmétique (c'est un peu compliquéet abstrait. mais c'est possible).



Les suites

Avec notre langage ensembliste, on peut définir une structure linéaire générique, modèle mathématique de structures de données que sont les listes, les tableaux, les files d'attentes, etc.

Une suite d'éléments de X, notée seq  X, est une fonction partielle d'un intervalle 1..n dans x :
seq  X º {S : IN1¬® X | $ n:IN. dom S = 1..n }
Le ième élément de S est simplement S(i) (i.e l'image de i par S). Notez que si iÏdom S cet élément n'est pas défini.

La fonction définie nulle part, c'est-à-dire, la fonction de domaine vide est aussi une suite car 1..0 = Ø. C'est la suite vide. On définit la suite vide comme l'ensemble vide (i.e. la fonction définie nulle part) :
áñ º Ø
Le fait que áñÎ seq  X est donnépar dom áñ = Ø = 1..0.

La longueur d'une suite est le nombre de ses éléments, son cardinal. C'est un nombre entier car les suites sont, par définition, des ensembles finis. On note : #S.

Il sera souvent utile, on définit l'ensemble des suites non vides par :
seq1  X º {s:seq  X | s¹áñ }


Le langage Z

Le langage Z se base sur le langage ensembliste pour offrir un format de spécification de fonctions ou d'opérations. C'est un avatar de la méthode de développement VDM.



Définitions axiomatiques

Les fonctions sont définies de façon axiomatiques : on introduit un symbole et on donne une formule énonçant les propriétés essentielles de ce symbole. Le format de définition axiomatique est :
nom : type
formule

Voici quelques exemples de spécifications de fonctions sur les suites.

Concaténation :
^ : seq  X × seq  X ® seq  X
" s1, s2Îseq  X. " iÎIN1.
  (i £ #s1Þ s1^ s2(i) = s1(i)) Ù
  (i > #s1Þ s1^ s2(i) = s2(i-#s1))

Sous suite :
sub : seq  X × IN × IN ® seq  X
" sÎseq  X. " i,jÎ IN.
  dom(sub(s,i,j)) = 1.. j-i+1
  " kÎdom(sub(s,i,j)). sub(s,i,j)(k) = s(k+i-1)

Suites vues comme des listes :
head:seq1  X® X
" sÎseq1  X.
  head(s) = s(1)
 
tail:seq1  X® seq  X
" sÎseq1  X.
  tail(s) = sub(s,2,#s)



Suites vues comme des files d'attentes :
last:seq1  X® X
" sÎseq1  X.
  last(s) = s(#s)
 
front:seq1  X® seq  X
" sÎseq1  X.
  front(s) = sub(s,1,#s-1)


Schémas d'opérations

Un schéma d'opération est, àla base un triplet constituéd'un nom, d'un ensemble de déclarations et d'une formule. Le format général d'un schéma d'opération est le suivant :
Nom
 
declarations
 
 
formule
 
Les déclarations sont de la forme x:Xx est une variable et X une expression ensembliste.

Ainsi considéré, un schéma représente un état : n-uplet de valeurs (les variables déclarées) possédant certaines propriétés (la formule). Cette formule peut exprimer des relations entre les variables constituant l'état. C'est pourquoi un schéma d'opération s'utilise aussi pour établir une relation entre un état avant (l'opération) et un état aprés (l'opération).

Par exemple, on définira l'opération de retournement d'une liste comme une opération Rev reliant une suite s àune suite s' oús' est le miroir de s :
 
Rev
 
 
 
s, s' : seq  X dom(s) = dom(s') Ù
" iÎ dom(s'), s'(i) = s(#s-i+1)

Cette façon de noter un état avant et aprés avec une apostrophe est devenue l'usage aussi l'a-t-on généralisée aux schemas.

Si le schéma S est  
S
 
x:T
 
 
j
 
  alors le schéma S' est  
S'
 
x':T
 
 
j [x'/x]
 

Un schéma peut se résumer àde simples déclarations auquel cas on laisse vide la section formule :
Nom
 
declarations
 
 
 

Les schémas peuvent se combiner de plusieurs manières : Et utilisant la combinaison par conjonction, on définit une schéma général de relation avant-aprés :
D S º SÙ S'


Un exemple

Les éléments de spécification qui suivent concernent la partie l'API de CICS qui permet de gérer les files d'attentes temporaires Temporary Storage Queue lors d'échanges de données entre divers sites d'un système d'information. L'ensemble de la spécification est donnée par schémas. Cet exemple est tiréde << Specifying the IBM CICS Application Programming Interface >>, par Steve King, in Specification Case Studies, Ian Hayes ed., Prentice Hall, deuxième édition, 1993.

Les données
Nous manipulerons des files d'attentes contenant des suites d'octets. On pose :
BYTE == 0..255
TSElem == seq  BYTE
Les files d'attentes du système (TSQ) sont modélisées par des suites (ar) et un pointeur (l'entier p) sur le dernier élément de la file ayant fait l'objet d'une opération de lecture ou d'écriture (voir infra) :
 
TSQ
 
 
 
ar:seq  TSElem
p:IN p £ #ar
On définit l'état initial d'une file d'attente :
 
TSQ_Initial
 
 
 
TSQ ar=áñ Ù p=0
Notez que la conjonction p £ #arÙ p=0, qui vient de l'inclusion de TSQ, est cohérente.

Les opérations
On spécifie les opérations d'ajout et de retrait d'un élément par les schémas Append0 et Remove0
 
Append0
 
 
 
D TSQ
from?:TSElem
item!:IN ar' = ar^áfrom?ñ Ù
item! = #arÙ
p' = p
   
 
Remove0
 
 
 
D TSQ
item!:TSElem p < #ar Ù
p' = p+1 Ù
into! = ar(p') Ù
ar' = ar
Le ? de from et le ! de item! sont conventionnels. Ils indiquent les << entrées-sorties >> du schéma. C'est àdire les valeurs dont il faut disposer pour l'opération (ici, l'éléent àajouter from?) et les valeurs données àl'issue de l'opération (ici, le nouveau nombre d'élément item!). Ces marques doivent être considérées comme des commentaires pour une implentation future. Ils n'ont pas de sens au niveau logique.

L'opération de retrait Remove0 a une précondition : le pointeur p ne doit pas être en fin de file. C'est ce qu'exprime p < #ar. En fin d'opération, il aura étéincrémenté : p' = p+1.

Les deux opérations suivantes utilisent les files comme des tableaux dont on peut modifier (Write0) ou consulter (Read0) une case :
 
Write0
 
 
 
D TSQ
item?:IN
from?:TSElem item?Î 1..#ar Ù
ar' = arÅ{item?|® from?} Ù
p' = p
   
 
Read0
 
 
 
D TSQ
item?:IN
into!:TSElem item?Î 1..#ar Ù
into! = ar(item?) Ù
p' = itemÙ
ar' = ar

Les erreurs
Nous avons vu qu'une opération comme Remove0 supposait une précondition. Si celle-ci n'est pas réalisée, il faut que l'opération le signale en positionnant un staut d'exécution. Pour ce, on se donne l'ensemble énumérésuivant :
OpStatus = {Success, ItemErr, NoSpace }
dont les éléments sont des constantes abstraites.

On donne dans un schéma les éléments communs àtoute opération sur le statut d'exécution :
 
ERROR
 
 
 
D TSQ
report! : OpStatus q TSQ' = q TSQ
L'opérateur q appliquéàun schéma donne le n-uplet de tous les noms de variables que contient le schéma. On l'utilise ici pour signifier que toutes des variables de TSQ doivent êtres égales àcelle de TSQ' (i.e. ar'=ar et p'=p).

Pour chaque statut possible, on donne une opération spécifique :
 
NoneLeft
 
 
 
ERROR p = #ar Ù
report! = ItemErr
 
 
OutOfBounds
 
 
 
ERROR
item?:IN item?Ï1..#ar Ù
report! = ItemErr
 
 
OutOfSpace
 
 
 
ERROR report! = NoSpace
 
Successful
 
 
 
report! : OpStatus report! = Success
Remarquez que les opérations NoneLeft et OutOfBounds expriment une condition pour que report! prenne une valeur donnée alors que OutOfSpace ne le fait pas. En effet, dans l'état actuel de la spécification, on ne dispose d'aucune information sur la taille de l'espace disponible pour stocker les files d'attente. La seule chose que l'on puisse faire, c'est de prévoir la possibilitéd'un débordement de l'espace de stockage disponible.

Redéfinitions avec gestion d'erreurs
Maintenant que les erreurs prévisibles ont étérépertoriées et spécifiées, on redéfinit les opérations sur les files d'attente en intégrant la gestion du statut d'exécution. On obtient ainsi par simple combinaison logique des schémas les opérations :
     
Append º (Append0 Ù Successful) Ú OutOfSpace
     
Remove º (Remove0 Ù Successful) Ú NoneLeft
     
Write º (Write0 Ù Successful) Ú OutOfBound Ú OutOfSpace
     
Read º (Read0 Ù Successful) Ú OutOfBound
     
Les schémas obtenus se lisent simplement : ou l'opération a réussi et son satut est Success, ou l'opération a échouéavec l'un des statut d'erreur associé.



Hoare-Floyd avec des spécifications ensemblistes

Préfixe d'une suite

Posons
Prefix(s1, s) º " iÎ dom(s1).(iÎ dom(s)Ù s1(i) = s(i))

On veut établir la validitédu triplet

[ s1Î seq  X Ù sÎ seq  X Ù iÎ IN ]
i := 1;
While (i
£ #s1) Ù (i £ #s) Ù s(i)=s1(i) do
  i := i+1
done
[ i > #s1 Þ Prefix(s1, s) ]
La post condition permet de déduire si oui ou non s1est préfixe de s.

On utilise l'invariant suivant : Prefix(sub(s1,1,i-1), s).

On a que :
s1Î seq  X Ù sÎ seq  X Ù iÎ IN Þ Prefix(sub(s1,1,0), s)
(puisque sub(s1,1,0)=áñ et Prefix(áñ, s) pour toute suite s).

On a donc, par règle de l'affectation :
[ Prefix(sub(s1,1,0) ]
i := 1
[ Prefix(sub(s1,1,i-1), s) ]
En utilisant la règle de la séquence, il faut montrer la validitéde la boucle :
[ Prefix(sub(s1,1,i-1), s) ]
While (i
£ #s1) Ù (i £ #s) Ù s(i)=s1(i) do
  i := i+1
done
[ i > #s1 Þ Prefix(s1, s) ]
Montrons que :
¬((i £ #s1) Ù (i £ #s) Ù (s(i)=s1(i))) Ù Prefix(sub(s1,1,i-1), s) Þ i > #s1 Þ Prefix(s1, s)

On suppose pour cela que
H1: ¬((i £ #s1) Ù (i £ #s) Ù (s(i)=s1(i)))
H2: Prefix(sub(s1,1,i-1), s)
H3: i > #s1
et on montre : Prefix(s1, s).
En effet, de H3, on peut déduire sub(s1,1,i-1)=sub(s1,1,#s1) ; et comme sub(s1,1,#s1)=s1, on obtient, par H2 que Prefix(s1, s).

En utilisant le affaiblissement de la postcondition, il faut donc montrer la validitéde

[ Prefix(sub(s1,1,i-1), s) ]
While (i
£ #s1) Ù (i £ #s) Ù s(i)=s1(i) do
  i := i+1
done
[ ¬((i £ #s1) Ù (i £ #s) Ù (s(i)=s1(i))) Ù Prefix(sub(s1,1,i-1), s) ]
C'est àdire, par la règle de la boucle

[ (i £ #s1) Ù (i £ #s) Ù (s(i)=s1(i)) Ù Prefix(sub(s1,1,i-1), s) ]
i := i+1
[ Prefix(sub(s1,1,i-1), s) ]
Or on a
(i £ #s1) Ù (i £ #s) Ù (s(i)=s1(i)) Ù Prefix(sub(s1,1,i-1), s) Þ Prefix(sub(s1,1,i), s)
(c'est facile àvérifier).

Comme de plus (i+1)-1=i, en utilisant le renforcement de la précondition, il faut montrer

[ Prefix(sub(s1,1,(i+1)-1), s) ]
i := i+1
[ Prefix(sub(s1,1,i-1), s) ]
Ce qui est immédiat pas la règle de l'affectation.



Occurence dans une suite

On veut établir la validitéde

[ sÎ seq  X Ù iÎ IN Ù eÎ X]
i := 1;
While (i
£ #s) Ù s(i)¹e do
  i := i+1
done
[ i £ #s Þ $ jÎdom(s). s(j)=e ]
Le schéma de preuve est trés proche de l'exemple précédent.

On considère l'invariant : " jÎdom(s). (j<iÞ s(j)¹e)

Par renforcement de la précondition et règle de l'affectation, on a
[ " jÎdom(s). (j<1Þ s(j)¹e) ]
i := 1
[ " jÎdom(s). (j<iÞ s(j)¹e) ]
Par règle de la séquence, il reste àmontrer que

[ " jÎdom(s). (j<iÞ s(j)¹e) ]
While (i
£ #s) Ù s(i)¹e do
  i := i+1
done
[ i £ #s Þ $ jÎdom(s). s(j)=e ]
Montrons que
¬((i £ #s) Ù s(i)¹e) Ù (" jÎdom(s). (j<iÞ s(j)¹e)) Þ (i £ #s) Þ $ jÎdom(s). s(j)=e
Pour ce, on suppose
H1: ¬((i £ #s) Ù s(i)¹e)
H2: " jÎdom(s). (j<iÞs(j)¹e)
H3: i £ #s
et on montre : $ jÎdom(s). s(j)=e.

L'hypotèse H1 est équivalente à¬(i £ #s) Ú s(i)=e ; combinée avec H3, on obtient que s(i)=e. Comme H3 nous donne aussi que iÎdom(s), on a trouvénotre j.

Par règle d'affaiblissement de la postcondition, il nous donc donc montrer que
[ " jÎdom(s). (j<iÞ s(j)¹e) ]
While (i
£ #s) Ù s(i)¹e do
  i := i+1
done
[ ¬((i £ #s) Ù s(i)¹e) Ù " jÎdom(s). (j<iÞ s(j)¹e) ]
C'est-à-dire, par règle de la boucle

[ (i £ #s) Ù s(i)¹e Ù " jÎdom(s). (j<iÞ s(j)¹e) ]
i := i+1
[ " jÎdom(s). (j<iÞ s(j)¹e) ]
Comme on a que
(i £ #s) Ù s(i)¹e Ù " jÎdom(s). (j<iÞ s(j)¹e) Þ " jÎdom(s). (j<i+1Þ s(j)¹e)
(c'est facile àvérifier).

En utilisant le renforcement de la précondition, il faut montrer

[ " jÎdom(s). (j<i+1Þ s(j)¹e) ]
i := i+1
[ " jÎdom(s). (j<iÞ s(j)¹e) ]
Ce qui est immédiat par la règle de l'affectation.




This document was translated from LATEX by HEVEA.