Preuve de programme

I. Bases logiques

A. Calcul des prédicats (du premier ordre)

Les formules de la logique du premier ordre sont des formules logiques dans lesquelles on peut trouver :

Exemple :

∀ x, ∀ y ∃ z.( x ∈ ℤ ∧ y ∈ ℤ ∧ z ∈ ℤ → (z ≥ 0 ∧ (x = y + z ∨ y = x + z)))
      

B. Un peu de vocabulaire

Une variable quantifiée (existentiellement ou universellement) est appelée variable liée.

Une variable non quantifiée est appelée variable libre.

Une formule close est une formule dont toutes les variables sont liées.

Un modèle M d'un langage du premier ordre est une structure (elements, fonctions, prédicats) dans lequel on donne un sens aux symboles du langage.

Une formule F est satisfaisable s'il existe un modèle M dans lequel F est vraie. On note alors M ⊧ F.

Une formule F est valide si et seulement si elle est vraie dans tout modèle du langage. On note cela ⊧ F.

Le calcul des prédicat est semi-décidable : il est possible d'énumérer tous les théorèmes, mais il n'existe pas d'algorithme capable de prouver à coup sûr n'importe quel théorème.

Sans les fonctions, le calcul des prédicats est complet : tout théorème est démontrable (théorème de complétude de Gödel). Cependant, si on rajoute les fonctions sur l'arithmétique, on obtient un système incomplet : il existe des théorèmes non démontrables (théorème d'incomplétude de Gödel).

C. Skolémisation

La skolémisation est un processus qui consiste à transformer une formule du calcul du prédicat de façon à :

Exemple pour la formule précédente :

  ∀ x, ∀ y ( x ∈ ℤ ∧ y ∈ ℤ ∧ z(x,y) ∈ ℤ → (z ≥ 0 ∧ (x = y + z ∨ y = x + z)))

Transformable éventulement en :

  ( x ∈ ℤ ∧ y ∈ ℤ ∧ z(x,y) ∈ ℤ → (z ≥ 0 ∧ (x = y + z ∨ y = x + z)))

D. Triplet de Hoare

un Triplet de Hoare se note {Pré} Inst {Post}, avec :

Il permet de spécifier que si la propriété Pré est vraie avant l'exécution de l'instruction Inst, alors la propriété Post est vraie à la fin de l'exécution de l'instruction Inst. Les triples de Hoare permettent de :

Exemples :

{x = n} (x = x+1) {x = n+1}
          
{x = n} (if b then x = n + 1 else x = n -1) {(b → x = n+1) ∧ (¬b → x = n - 1)}
          

Les notions de pré-condition et post-condition sont étendues aux fonctions/méthodes ainsi :

Remarque : les préconditions et postconditions sont en fait des formules de la logique des prédicats skolémisées.

E. Substitution

Appliquer une substitution est une opération qui consiste à remplacer une ou plusieurs variables libres dans un prédicat par une valeur. On note en général une substitution {val1/var1, val2/var2, ...}.

Exemple :

{4/x, 5/y}(x + 1 = y) ≡ (4 + 1 = 5)

Mais on peut également noter une substitution [var1 := val1 || var2 := val2 ...].

Exemple :

[x := 4 || y := 5](x + 1 = y) ≡ (4 + 1 = 5)

En effet, une affectation n'est ni plus ni moins qu'une substitution...

Ainsi, à partir d'une précondition d'une fonction, on peut, instruction après instruction, établir la nouvelle propriété établie par l'instruction, et ainsi, vérifier si la fonction établit bien sa post condition.

Exemple :

f(x):
  PRE:
    x ≥ 0
  CORPS:
    x = x + 1; PROP : x ≥ 1
    x = 2 * x; PROP : x ≥ 2
    x = x - 1
  POST:
    x ≥ 1
  
Preuve de PROP 1 :
    x ≥ 0 → [x := x+1](x ≥ 1)
    x ≥ 0 → (x+1 ≥ 1)

Preuve de PROP 2 :
    x ≥ 1 → [x := 2x](x ≥ 2)
    x ≥ 1 → (2x ≥ 2)

Preuve de POST :
    x ≥ 2 → [x := x - 1](x ≥ 1)
    x ≥ 2 → (x-1 ≥ 1)

II. Invariant

A. Notion d'Invariant de boucle

Comment utiliser les mécanismes précédents avec une boucle ?

On introduit un invariant de boucle : prédicat vrai à chaque itération, mais tel que, conjugué à la condition de sortie de la boucle, il nous permette d'établir la post-condition.

Exemple :

def maximum(liste):
  max = liste[0]
  for i in range(0, len(liste)):
    if liste[i] > max:
      max = liste[i]
    #Invariant de boucle : max = maximum(liste[0:i])
  return max

B. Invariant de boucle et preuve

Principe de la preuve d'un invariant de boucle = preuve par récurrence :

L'invariant de boucle doit être là pour établir la post-condition. Il faudra donc que la formule suivante soit vraie :

PRE ∧ FIN_BOUCLE ∧ INV → POST

C. Invariant de classe

L'utilisation d'un objet d'une classe (avec encapsulation) peut être vue comme une suite d'appels des méthodes de l'objet, ce qui revient à comparer cela à une boucle sur un choix d'une méthode parmi toutes celles définies dans la classe. On peut donc associer un invariant à une classe. La preuve de cet invariant consistera à prouver que :

III. Preuve de correction partielle

Les principes vus précédemment permettent de vérifier que si on programme correct termine, alors il établit sa post-condition. C'est ce qu'on appelle la correction partielle.

Cela ne permet nullement de prouver que le programme se termine effectivement.

IV. Correction Totale

A. Définition

Pour qu'un programme soit réellement correct, il faut qu'il soit partiellement correct, et qu'il se termine. C'est ce qu'on appelle la correction totale.

B. Principe de preuve de terminaison

Pour prouver qu'une boucle termine, on va associer à la boucle un Variant.

C. Définitions

1. Variant

Un variant est une suite strictement décroissante définie sur une structure bien fondée.

2. Structure bien fondée

Une structure bien fondée est une structure (E, ≤) dans laquelle toute suite strictement décroissante possède une borne inférieure.

Exemple fréquemment utilisé de structure bien fondée : (ℕ, ≤).

d. Détail du processus de preuve

On vérifie qu'avant la première itération, le variant est bien défini (par exemple, est un naturel) :

PRE → V ∈ ℕ

On vérifie que la valeur du variant décroît et reste bien typée entre 2 itérations :

V = v ∧ INV → [CORPS_BOUCLE](V < v)

V. Applications

A. Préambule : écrire les pré-conditions, post-conditions et invariant en Python

Il est possible d'écrire ces différents prédicats dans un programme Python en utilisant les assertions. Cela n'a aucune valeur de preuve mais permet :

Note sur les assertions en Python :

En Python, une assertion s'écrit sous l'une des formes suivantes :

lorsque l'instruction d'assertion est exécutée, l'expression à valeur booléenne ExprBool est évaluée. Si elle est vraie, le programme continue de s'exécuter. Sinon, le programme s'arrête avec une exception et l'éventuel message est affiché.

1. Écriture des préconditions

Clause assert au début des méthodes

2. Écriture des postconditions

Clause assert à la fin des méthodes

3. Écriture d'un invariant

B. Exemple

preuve01personne.py

C. Exercices

1. Preuves d'invariants de classe et de post-conditions

2. Preuves sur les boucles

3. Travail personnel

Faire la preuve de correction totale pour la version non optimisée du tri à bulle donnée dans le fichier preuve06TriABulleNaif.py