Aller au contenu
roomi fields

Langages formels · L8 · Cours

Sémantique axiomatique

Découvrez comment la logique de Hoare permet de raisonner formellement sur la correction des programmes.

Publié le 8 février 2026 · mis à jour 19 février 2026 · Read in English

Prouver qu’un programme est correct

Un programme qui s’exécute sans erreur visible n’est pas nécessairement correct. Seule une preuve mathématique peut le garantir. La sémantique axiomatique fournit les outils pour y parvenir.

Où se situe cet article ?

Dans L5, nous avons vu trois façons de donner du sens à un programme :

  • Opérationnelle : “comment ça s’exécute” — approfondie dans L6
  • Dénotationnelle : “quelle fonction mathématique ça calcule” — approfondie dans L7
  • Axiomatique : “quelles propriétés ça garantit” — c’est le sujet de cet article

Avec L8, la trilogie sémantique est complète. Chaque approche a ses forces : la SOS (Structural Operational Semantics, sémantique opérationnelle structurelle) décrit le comment, la dénotationnelle le quoi, et l’axiomatique le pourquoi c’est correct.


Pourquoi c’est important ?

Les tests peuvent montrer la présence de bugs, jamais leur absence. Un programme peut passer des milliers de tests et contenir une erreur subtile qui ne se manifeste que dans un cas particulier. Les sondes spatiales, les systèmes de freinage, les protocoles cryptographiques — dans ces domaines, “ça a l’air de marcher” ne suffit pas.

La sémantique axiomatique, fondée sur la logique de Hoare, permet de prouver qu’un programme est correct : qu’il fait exactement ce qu’il doit faire, pour toutes les entrées possibles, pas seulement celles qu’on a testées. Cette approche a donné naissance au Design by Contract (conception par contrat — Eiffel, Ada/SPARK), aux contrats intégrés dans les langages modernes, et aux prouveurs de programmes comme Dafny ou Frama-C (outils de vérification automatique de code).

L’idée en une phrase

La sémantique axiomatique décrit le sens d’un programme par les propriétés logiques qu’il préserve : si certaines conditions sont vraies avant l’exécution, d’autres le seront après.


Expliquons pas à pas

Exemple 1 : Le distributeur de café (reprise)

Reprenons le distributeur de café de L5 pour voir comment l’approche axiomatique raisonne sur ce système.

En sémantique opérationnelle, on décrivait les transitions :

(attente, 0€) → (attente, 1€) → (prêt, 2€) → (distribution, 0€)

En sémantique dénotationnelle, on décrivait la fonction :

[[distributeur]](2€, café) = café

En sémantique axiomatique, on décrit ce que le système garantit :

{argent≥2€  ∧  bouton=cafeˊ}appuyer{cafeˊ_distribueˊ=vrai  ∧  argent=0€}\{\text{argent} \geq 2€ \;\wedge\; \text{bouton} = \text{café}\} \quad \text{appuyer} \quad \{\text{café\_distribué} = \text{vrai} \;\wedge\; \text{argent} = 0€\}

Décryptage :

  • {argent≥2€  ∧  bouton=cafeˊ}\{\text{argent} \geq 2€ \;\wedge\; \text{bouton} = \text{café}\} : la précondition — ce qui doit etre vrai avant
  • appuyer\text{appuyer} : la commande — l’action exécutée
  • {cafeˊ_distribueˊ=vrai  ∧  argent=0€}\{\text{café\_distribué} = \text{vrai} \;\wedge\; \text{argent} = 0€\} : la postcondition — ce qui est garanti après
  • ∧\wedge : conjonction logique (“et”)

Lecture : “Si l’utilisateur a inséré au moins 2 euros ET a appuyé sur le bouton café, ALORS le café sera distribué ET le compteur d’argent sera remis à zéro.”

On ne dit pas comment la machine fonctionne (ce sont des engrenages ? un micro-contrôleur ?). On ne dit pas quelle fonction elle calcule. On dit uniquement : voici le contrat — ce qui est garanti si les conditions d’entrée sont remplies.

C’est exactement l’esprit de la sémantique axiomatique : raisonner sur les spécifications, pas sur l’implémentation.


Les triplets de Hoare

Le formalisme central de la sémantique axiomatique est le triplet de Hoare :

{P}  C  {Q}\{P\}\; C \;\{Q\}

Qu’est-ce qu’un triplet de Hoare ?

Inventé par Tony Hoare en 1969, c’est une notation qui lie trois éléments :

  • PP (précondition) : une assertion logique sur l’état du programme avant l’exécution
  • CC (commande) : l’instruction ou le programme à exécuter
  • QQ (postcondition) : une assertion logique sur l’état du programme après l’exécution

Le triplet {P}  C  {Q}\{P\}\; C \;\{Q\} se lit : “si PP est vrai avant d’exécuter CC, et si CC termine, alors QQ est vrai après.”

Exemples concrets :

{x=5}  x:=x+1  {x=6}\{x = 5\}\; x := x + 1 \;\{x = 6\}

Lecture : “Si xx vaut 55 avant l’affectation, alors xx vaut 66 après.”

{x>0}  x:=x×2  {x>0}\{x > 0\}\; x := x \times 2 \;\{x > 0\}

Lecture : “Si xx est positif avant, alors xx reste positif après la multiplication par 22.”

{vrai}  x:=7  {x=7}\{\text{vrai}\}\; x := 7 \;\{x = 7\}

Lecture : “Quel que soit l’état initial (la précondition vrai\text{vrai} est toujours satisfaite), après x:=7x := 7, xx vaut 77.”

La notation {vrai}\{\text{vrai}\} comme précondition : La précondition vrai\text{vrai} (ou true\text{true}) signifie “aucune condition requise” — le triplet est valable quel que soit l’état de départ. C’est la précondition la plus faible possible.


Les axiomes et règles de la logique de Hoare

La puissance de la logique de Hoare réside dans un petit ensemble de règles qui permettent de construire des preuves de correction pour des programmes complets, brique par brique.

Axiome d’affectation

C’est la règle la plus fondamentale — et la plus surprenante au premier abord :

{Q[x/e]}  x:=e  {Q}[Affectation]\dfrac{}{\{Q[x/e]\}\; x := e \;\{Q\}} \quad \text{[Affectation]}

Règle 1 — Axiome d’affectation : pas de prémisse au-dessus de la barre (c’est un axiome). La précondition s’obtient par substitution dans la postcondition.

Décryptage :

  • Q[x/e]Q[x/e] (lu ”QQ où xx est remplacé par ee”) : on prend la postcondition QQ et on remplace chaque occurrence de xx par l’expression ee
  • Pas de prémisse au-dessus de la barre : c’est un axiome (toujours vrai, sans condition)
  • La règle se lit “à l’envers” : on part de la postcondition QQ et on remonte vers la précondition

Pourquoi à l’envers ? C’est contre-intuitif, mais logique : pour savoir ce qui doit etre vrai avant l’affectation, on regarde ce qu’on veut après et on “défait” l’affectation.

Exemple :

On veut prouver {? ⁣? ⁣?}  x:=x+1  {x=6}\{?\!?\!?\}\; x := x + 1 \;\{x = 6\}.

Appliquons l’axiome : la postcondition est x=6x = 6, donc la précondition est (x=6)[x/(x+1)](x = 6)[x / (x+1)], c’est-à-dire on remplace xx par x+1x + 1 dans x=6x = 6 :

(x=6)[x/(x+1)]=(x+1=6)=(x=5)(x = 6)[x / (x + 1)] = (x + 1 = 6) = (x = 5)

Résultat :

{x=5}  x:=x+1  {x=6}[Affectation]\dfrac{}{\{x = 5\}\; x := x + 1 \;\{x = 6\}} \quad \text{[Affectation]}

Si x=5x = 5 avant, alors x=6x = 6 après x:=x+1x := x + 1.

Vérification intuitive : si x vaut 5 avant, et qu’on ajoute 1, alors x vaut bien 6 après. L’axiome fonctionne.

Autre exemple :

{? ⁣? ⁣?}  x:=x×2  {x>0}\{?\!?\!?\}\; x := x \times 2 \;\{x > 0\}

Q=(x>0)Q = (x > 0), donc Q[x/(x×2)]=(x×2>0)=(x>0)Q[x / (x \times 2)] = (x \times 2 > 0) = (x > 0)

{x>0}  x:=x×2  {x>0}[Affectation]\dfrac{}{\{x > 0\}\; x := x \times 2 \;\{x > 0\}} \quad \text{[Affectation]}

Multiplier un positif par 2 le garde positif. Précondition et postcondition sont identiques.

Ici, la précondition et la postcondition sont identiques : multiplier un nombre positif par 2 le garde positif.


Règle de séquence

Quand on enchaîne deux instructions, on compose les preuves :

{P}  c1  {R}{R}  c2  {Q}{P}  c1; c2  {Q}[Seˊquence]\dfrac{\{P\}\; c_1 \;\{R\} \qquad \{R\}\; c_2 \;\{Q\}}{\{P\}\; c_1;\,c_2 \;\{Q\}} \quad \text{[Séquence]}

Règle 2 — Séquence : composition de deux preuves via une assertion intermédiaire RR.

Décryptage :

  • RR est une assertion intermédiaire : la postcondition de c1c_1 et la précondition de c2c_2
  • La règle dit : si c1c_1 transforme PP en RR, et c2c_2 transforme RR en QQ, alors la séquence c1; c2c_1;\,c_2 transforme PP en QQ
  • C’est une composition de preuves, comme enchaîner deux maillons d’une chaîne logique

Exemple :

Prouvons {x=0}  x:=x+1;  x:=x×3  {x=3}\{x = 0\}\; x := x + 1 ;\; x := x \times 3 \;\{x = 3\} :

Étape 1 — Axiome d’affectation pour x:=x×3x := x \times 3 avec Q=(x=3)Q = (x = 3) :

Q[x/(x×3)]=(x×3=3)=(x=1)⇒{x=1}  x:=x×3  {x=3}Q[x / (x \times 3)] = (x \times 3 = 3) = (x = 1) \quad \Rightarrow \quad \{x = 1\}\; x := x \times 3 \;\{x = 3\}

Étape 2 — Axiome d’affectation pour x:=x+1x := x + 1 avec Q=(x=1)Q = (x = 1) :

Q[x/(x+1)]=(x+1=1)=(x=0)⇒{x=0}  x:=x+1  {x=1}Q[x / (x + 1)] = (x + 1 = 1) = (x = 0) \quad \Rightarrow \quad \{x = 0\}\; x := x + 1 \;\{x = 1\}

Étape 3 — Règle de séquence avec R=(x=1)R = (x = 1) :

{x=0}  x:=x+1  {x=1}[Aff]{x=1}  x:=x×3  {x=3}[Aff]{x=0}  x:=x+1; x:=x×3  {x=3}[Seˊquence]\dfrac{\dfrac{}{\{x = 0\}\; x := x + 1 \;\{x = 1\}} \text{[Aff]} \qquad \dfrac{}{\{x = 1\}\; x := x \times 3 \;\{x = 3\}} \text{[Aff]}}{\{x = 0\}\; x := x + 1;\, x := x \times 3 \;\{x = 3\}} \quad \text{[Séquence]}

Arbre de preuve complet : deux axiomes d’affectation (en haut) composés par la règle de séquence (en bas).

Observation : on travaille “de droite à gauche” — on commence par la dernière instruction et on remonte. C’est la méthode naturelle avec l’axiome d’affectation.


Règle du if

{P∧b}  c1  {Q}{P∧¬b}  c2  {Q}{P}  if  b  then  c1  else  c2  {Q}[If]\dfrac{\{P \wedge b\}\; c_1 \;\{Q\} \qquad \{P \wedge \neg b\}\; c_2 \;\{Q\}}{\{P\}\; \mathbf{if}\; b \;\mathbf{then}\; c_1 \;\mathbf{else}\; c_2 \;\{Q\}} \quad \text{[If]}

Règle 3 — If : les deux branches doivent aboutir à la même postcondition QQ.

Décryptage :

  • P∧bP \wedge b : la précondition PP est vraie ET la condition bb est vraie (on entre dans la branche then)
  • P∧¬bP \wedge \lnot b : la précondition PP est vraie ET la condition bb est fausse (on entre dans la branche else)
  • ¬b\lnot b : négation de bb (”bb est faux”)
  • Les deux branches doivent aboutir à la même postcondition QQ
  • La règle dit : peu importe la branche prise, on arrive à QQ

Exemple :

Prouvons {vrai}  if  x≥0  then  y:=x  else  y:=−x  {y=∣x∣}\{\text{vrai}\}\; \mathbf{if}\; x \geq 0 \;\mathbf{then}\; y := x \;\mathbf{else}\; y := -x \;\{y = |x|\} :

Branche then (x≥0x \geq 0) : {vrai∧x≥0}  y:=x  {y=∣x∣}\{\text{vrai} \wedge x \geq 0\}\; y := x \;\{y = |x|\} Si x≥0x \geq 0, alors x=∣x∣x = |x|, donc y:=xy := x donne y=∣x∣y = |x|.

Branche else (x<0x < 0) : {vrai∧¬(x≥0)}  y:=−x  {y=∣x∣}\{\text{vrai} \wedge \lnot(x \geq 0)\}\; y := -x \;\{y = |x|\} Si x<0x < 0, alors −x=∣x∣-x = |x|, donc y:=−xy := -x donne y=∣x∣y = |x|.

{vrai∧x≥0}  y:=x  {y=∣x∣}{vrai∧x<0}  y:=−x  {y=∣x∣}{vrai}  if  x≥0  then  y:=x  else  y:=−x  {y=∣x∣}[If]\dfrac{\{\text{vrai} \wedge x \geq 0\}\; y := x \;\{y = |x|\} \qquad \{\text{vrai} \wedge x < 0\}\; y := -x \;\{y = |x|\}}{\{\text{vrai}\}\; \mathbf{if}\; x \geq 0 \;\mathbf{then}\; y := x \;\mathbf{else}\; y := -x \;\{y = |x|\}} \quad \text{[If]}

Preuve de la valeur absolue : les deux branches garantissent y=∣x∣y = |x|.

Ce programme calcule bien la valeur absolue de x, et la preuve le confirme formellement.


Règle du while et invariant de boucle

C’est la règle la plus puissante — et la plus difficile :

{I∧b}  c  {I}{I}  while  b  do  c  {I∧¬b}[While]\dfrac{\{I \wedge b\}\; c \;\{I\}}{\{I\}\; \mathbf{while}\; b \;\mathbf{do}\; c \;\{I \wedge \neg b\}} \quad \text{[While]}

Règle 4 — While : l’invariant II est préservé à chaque itération, et la négation de bb est acquise à la sortie.

Décryptage :

  • II est l’invariant de boucle : une propriété qui reste vraie à chaque itération
  • La prémisse dit : si l’invariant II est vrai ET la condition bb est vraie, alors après une itération de cc, l’invariant II est toujours vrai
  • La conclusion dit : si II est vrai au départ, alors à la sortie de la boucle (quand bb devient faux), on a I∧¬bI \wedge \lnot b

Analogie : L’invariant est comme un équilibriste sur un fil. À chaque pas (itération), il peut osciller (l’état change), mais il reste toujours sur le fil (l’invariant est préservé). Quand il s’arrête (la boucle termine), on sait qu’il est toujours sur le fil (II) et qu’il a atteint l’autre côté (¬b\lnot b).

L’invariant est la clé. Trouver le bon invariant est souvent la partie la plus difficile d’une preuve de correction. C’est un art autant qu’une science.


Règle de conséquence

Cette règle permet d’ajuster les préconditions et postconditions :

P′⇒P{P}  C  {Q}Q⇒Q′{P′}  C  {Q′}[Conseˊquence]\dfrac{P' \Rightarrow P \qquad \{P\}\; C \;\{Q\} \qquad Q \Rightarrow Q'}{\{P'\}\; C \;\{Q'\}} \quad \text{[Conséquence]}

Règle 5 — Conséquence : renforcement de la précondition et affaiblissement de la postcondition.

Décryptage :

  • P′⟹PP' \Longrightarrow P : P′P' est plus forte que PP (P′P' implique PP) — on renforce la précondition
  • Q⟹Q′Q \Longrightarrow Q' : QQ implique Q′Q' — on affaiblit la postcondition
  • Si on sait que {P}  C  {Q}\{P\}\; C \;\{Q\}, et qu’on dispose de conditions plus fortes en entrée ou plus faibles en sortie, le triplet reste valide

Intuition : Si un programme est correct sous certaines conditions, il l’est a fortiori sous des conditions plus restrictives (précondition renforcée) ou avec des garanties moindres (postcondition affaiblie).

Exemple :

On sait que {x=5}  x:=x+1  {x=6}\{x = 5\}\; x := x + 1 \;\{x = 6\}.

Or x=6x = 6 implique x>0x > 0. Par la règle de conséquence :

{x=5}  x:=x+1  {x=6}(x=6)⇒(x>0){x=5}  x:=x+1  {x>0}[Conseˊquence]\dfrac{\{x = 5\}\; x := x + 1 \;\{x = 6\} \qquad (x = 6) \Rightarrow (x > 0)}{\{x = 5\}\; x := x + 1 \;\{x > 0\}} \quad \text{[Conséquence]}

Affaiblissement de la postcondition : de x=6x = 6 (précis) vers x>0x > 0 (plus faible mais correct).

On a affaibli la postcondition : au lieu de garantir que xx vaut exactement 66, on garantit seulement qu’il est positif. C’est moins précis, mais toujours correct.


Les invariants de boucle

Qu’est-ce qu’un invariant de boucle ?

Un invariant de boucle est une propriété logique qui est :

  1. Vraie avant la première itération (initialisation)
  2. Préservée par chaque itération (conservation)
  3. Utile à la sortie : combinée avec la condition d’arret, elle donne la postcondition souhaitée

C’est le “fil rouge” qui traverse toutes les itérations et permet de conclure que le résultat est correct.

Exemple : somme des entiers de 1 à n

Considérons ce programme qui calcule la somme 1+2+…+n1 + 2 + \ldots + n :

s := 0 ;
i := 1 ;
while i ≤ n do
    s := s + i ;
    i := i + 1

Postcondition souhaitée : {s=n×(n+1)/2}\{s = n \times (n + 1) / 2\}

Trouver l’invariant :

L’idée est que ss contient toujours la somme des entiers de 11 à i−1i - 1. Formellement :

I:  s=(i−1)×i2  ∧  1≤i≤n+1I :\; s = \frac{(i - 1) \times i}{2} \;\wedge\; 1 \leq i \leq n + 1

Comment trouver un invariant ?

Quelques heuristiques :

  1. Regarder la postcondition : l’invariant est souvent une “version partielle” du résultat final
  2. Remplacer une constante par une variable : dans s=n(n+1)/2s = n(n+1)/2, remplacer nn par la variable de boucle i−1i - 1
  3. Vérifier sur les cas limites : l’invariant est-il vrai au début (i=1i = 1, s=0s = 0) ? À la fin (i=n+1i = n + 1) ?

Preuve de correction :

1. Initialisation : Avant la boucle, s=0s = 0 et i=1i = 1.

I=(0=(1−1)×12)∧(1≤1≤n+1)=(0=0)∧(1≤1≤n+1)=vrai(n≥0)I = \left(0 = \frac{(1 - 1) \times 1}{2}\right) \wedge (1 \leq 1 \leq n + 1) = (0 = 0) \wedge (1 \leq 1 \leq n + 1) = \text{vrai} \quad (n \geq 0)

2. Conservation : On suppose I∧i≤nI \wedge i \leq n et on exécute le corps :

  • Avant : s=(i−1)×i/2  ∧  i≤ns = (i - 1) \times i / 2 \;\wedge\; i \leq n
  • Après s:=s+is := s + i : snew=(i−1)×i/2+i=i×(i+1)/2s_{\text{new}} = (i - 1) \times i / 2 + i = i \times (i + 1) / 2
  • Après i:=i+1i := i + 1 : inew=i+1i_{\text{new}} = i + 1

Vérification de II avec les nouvelles valeurs :

snew=(inew−1)×inew2  ⇔  i×(i+1)2=((i+1)−1)×(i+1)2=i×(i+1)2✓s_{\text{new}} = \frac{(i_{\text{new}} - 1) \times i_{\text{new}}}{2} \;\Leftrightarrow\; \frac{i \times (i + 1)}{2} = \frac{((i + 1) - 1) \times (i + 1)}{2} = \frac{i \times (i + 1)}{2} \quad \checkmark

3. Terminaison de la boucle : Quand la boucle s’arrete, ¬(i≤n)\lnot(i \leq n), donc i=n+1i = n + 1 (car ii augmente de 11 à chaque itération et i≤n+1i \leq n + 1 par l’invariant).

I∧¬(i≤n):s=(i−1)×i2  ∧  i=n+1⇒s=n×(n+1)2✓I \wedge \lnot(i \leq n) :\quad s = \frac{(i - 1) \times i}{2} \;\wedge\; i = n + 1 \quad\Rightarrow\quad s = \frac{n \times (n + 1)}{2} \quad \checkmark

La preuve est complète : le programme calcule bien la somme des entiers de 11 à nn.


Correction partielle vs correction totale

Jusqu’ici, nous avons parlé de correction partielle. Il existe une distinction importante :

Correction partielleCorrection totale
Notation{P}  C  {Q}\{P\}\; C \;\{Q\}[P]  C  [Q][P]\; C \;[Q]
SignificationSi C termine, alors Q est vraiC termine ET Q est vrai
TerminaisonNon garantieGarantie
DifficultéPlus simplePlus difficile

Qu’est-ce que la correction partielle ?

Le triplet {P}  C  {Q}\{P\}\; C \;\{Q\} dit : “Si PP est vrai au départ ET si CC finit par terminer, alors QQ sera vrai.” Mais il ne dit rien sur la terminaison. Si CC boucle indéfiniment, le triplet est vacuously true (trivialement vrai, car la condition “si CC termine” n’est jamais remplie).

Exemple : {vrai}  while  true  do  skip  {faux}\{\text{vrai}\}\; \mathbf{while}\;\text{true}\;\mathbf{do}\;\text{skip} \;\{\text{faux}\} est un triplet de correction partielle valide. Puisque la boucle ne termine jamais, la postcondition faux\text{faux} n’est jamais atteinte, et le triplet n’est jamais violé.

Qu’est-ce que la correction totale ?

Le triplet [P]  C  [Q][P]\; C \;[Q] (avec des crochets au lieu d’accolades) dit : “Si PP est vrai, alors CC termine ET QQ est vrai après.” C’est une garantie plus forte : on prouve à la fois que le résultat est correct ET que le calcul se termine.

Prouver la terminaison : les variants de boucle

Pour passer de la correction partielle à la correction totale, il faut prouver la terminaison. L’outil principal est le variant (ou fonction de terminaison).

Qu’est-ce qu’un variant de boucle ?

Un variant est une expression à valeurs entières qui :

  1. Est positive (ou nulle) à chaque itération
  2. Décroît strictement à chaque itération

Puisqu’un entier ne peut pas décroître indéfiniment tout en restant positif, la boucle doit terminer.

Exemple : Pour la boucle de la somme, le variant est n+1−in + 1 - i :

  • Avant la boucle : n+1−1=n≥0n + 1 - 1 = n \geq 0 (si n≥0n \geq 0)
  • À chaque itération : ii augmente de 11, donc n+1−in + 1 - i décroît de 11
  • Quand le variant atteint 00 : i=n+1i = n + 1, la condition i≤ni \leq n est fausse, la boucle s’arrete

Invariant vs variant :

  • Invariant : ce qui ne change pas (propriété préservée) — prouve la correction
  • Variant : ce qui change (quantité qui décroît) — prouve la terminaison
  • Les deux ensemble donnent la correction totale

Application à BP3

Que pourrait-on prouver axiomatiquement sur une grammaire BP3 ?

Terminaison des grammaires :

En BP3, une grammaire peut boucler indéfiniment si une règle est récursive sans condition d’arret :

RND[1]
gram#1[1] S --> do4 S
gram#1[2] S --> do4

En sémantique axiomatique, on pourrait formuler :

{poids(gram#1[1])=w  ∧  w>0}deˊriver(S){nombre_deˊrivations≤f(w)}\{\text{poids}(\text{gram\#1}[1]) = w \;\wedge\; w > 0\} \quad \text{dériver}(S) \quad \{\text{nombre\_dérivations} \leq f(w)\}

Avec des poids décrémentaux (<50-12>), le variant serait la somme des poids des règles récursives : elle décroît à chaque application jusqu’à atteindre zéro, forçant la terminaison.

Propriétés musicales :

On pourrait exprimer des invariants musicaux :

{tessiture⊆[do3,do6]}deˊriver_complet(Grammaire){∀ note∈reˊsultat:do3≤note≤do6}\{\text{tessiture} \subseteq [\text{do3}, \text{do6}]\} \quad \text{dériver\_complet}(\text{Grammaire}) \quad \{\forall\, \text{note} \in \text{résultat} : \text{do3} \leq \text{note} \leq \text{do6}\}

“Si toutes les notes terminales de la grammaire sont dans la tessiture [do3, do6], alors le résultat restera dans cette tessiture.”

Pourquoi c’est la sémantique la moins utilisée pour BP3 :

  1. Pas de boucles classiques : BP3 utilise la réécriture récursive, pas des boucles while. L’axiome d’affectation ne s’applique pas directement.
  2. Stochasticité (caractère probabiliste du processus de dérivation) : la logique de Hoare classique est déterministe. Prouver des propriétés sur un processus aléatoire demande des extensions probabilistes (logiques de Hoare probabilistes).
  3. Complexité : les preuves axiomatiques pour des grammaires stochastiques à poids décrémentaux seraient très lourdes.

La sémantique opérationnelle (L6) reste l’outil le plus naturel pour BP3. Pour une exploration détaillée, voir la série B.


Tableau comparatif des trois sémantiques

CritèreOpérationnelle (SOS)DénotationnelleAxiomatique (Hoare)
QuestionComment ça s’exécute ?Quelle fonction ça calcule ?Qu’est-ce que ça garantit ?
FormalismeRègles de transitionFonctions sur domainesPré/postconditions
Notation⟨e,σ⟩⟹⟨e′,σ′⟩\langle e, \sigma \rangle \Longrightarrow \langle e', \sigma' \rangle⟦e⟧:D→D′\llbracket e \rrbracket : D \to D'{P}  C  {Q}\{P\}\; C \;\{Q\}
ForcesProche du code, exécutableCompositionnelle, abstraitePreuves de correction
FaiblessesVerbeux, bas niveauDifficile pour les effetsAnnotations manuelles
Idéal pourInterpréteurs, débogueursOptimisation, équivalenceVérification, contrats
Gère la stochasticitéNaturellement (via probabilités)Via monades probabilistesExtensions nécessaires
Gère l’état mutableNaturellement (via configurations)Via monades d’étatNaturellement (via assertions)
Pour BP3Choix principal (SOS)Possible mais complexeLimité (pas de boucles classiques)

Outils modernes de vérification formelle

La logique de Hoare est restée longtemps un outil théorique. Depuis les années 2000, des outils pratiques ont rendu la vérification formelle accessible.

Prouveurs interactifs :

  • Coq (France, INRIA) : prouveur de théorèmes basé sur la théorie des types. Utilisé pour certifier le compilateur CompCert (compilateur C formellement vérifié), les fondements mathématiques (théorème des quatre couleurs), et des protocoles cryptographiques
  • Isabelle/HOL (Cambridge/Munich) : prouveur général utilisé dans la vérification de systèmes d’exploitation (seL4, micro-noyau formellement vérifié) et de protocoles
  • Lean (Microsoft Research) : prouveur récent, très actif dans la communauté mathématique (projet Mathlib)

Vérification intégrée aux langages :

  • SPARK/Ada : sous-ensemble d’Ada avec des contrats (préconditions, postconditions, invariants) vérifiés statiquement. Utilisé dans l’aéronautique, le ferroviaire et la défense
  • Dafny (Microsoft Research) : langage avec vérification automatique. On écrit les invariants, Dafny prouve automatiquement la correction
  • Eiffel : pionnier du Design by Contract (Bertrand Meyer, 1986). Les préconditions (require), postconditions (ensure) et invariants de classe (invariant) sont intégrés au langage

Design by Contract :

L’idée de Bertrand Meyer est d’appliquer les triplets de Hoare directement dans le code :

-- Exemple en Eiffel (syntaxe simplifiée)
retirer (montant : INTEGER)
    require
        montant > 0
        solde >= montant
    do
        solde := solde - montant
    ensure
        solde = old solde - montant
        solde >= 0
    end

Ce style a été adopté (sous diverses formes) par Java (@Contract), Python (assert), Kotlin (require/ensure), et bien d’autres.


Un peu d’histoire

Robert Floyd (1967) pose les fondations avec sa méthode des “assertions intermédiaires” sur les organigrammes. L’idée : annoter chaque point d’un programme avec une assertion logique et vérifier que les transitions préservent ces assertions. Floyd est le premier à formaliser la notion d’invariant de boucle.

C.A.R. Hoare (1969) publie “An Axiomatic Basis for Computer Programming” dans Communications of the ACM. Il distille les idées de Floyd en un système déductif élégant : les triplets {P}  C  {Q}\{P\}\; C \;\{Q\} et un petit ensemble de règles (affectation, séquence, if, while, conséquence). L’article, d’une concision remarquable (12 pages), est l’un des plus cités de l’informatique.

Edsger Dijkstra (1975-1976) renverse la perspective avec le calcul des plus faibles préconditions (weakest preconditions). Au lieu de demander “cette précondition est-elle suffisante ?”, il demande “quelle est la précondition la plus faible qui garantit cette postcondition ?”. La notation wp(C,Q)\text{wp}(C, Q) donne la plus faible précondition PP telle que {P}  C  {Q}\{P\}\; C \;\{Q\}. Son livre A Discipline of Programming (1976) systématise cette approche et influence profondément le génie logiciel.

wp et la logique de Hoare : Le calcul wp\text{wp} de Dijkstra est en quelque sorte la version “mécanisée” de la logique de Hoare. Pour l’axiome d’affectation, wp(x:=e, Q)=Q[x/e]\text{wp}(x := e,\, Q) = Q[x/e] — exactement la règle de Hoare, mais formulée comme un calcul automatique plutôt qu’une déduction manuelle. C’est ce calcul qui est à la base des vérificateurs automatiques modernes comme Dafny.


Ce qu’il faut retenir

  1. Triplet de Hoare {P}  C  {Q}\{P\}\; C \;\{Q\} : si PP est vrai avant et CC termine, alors QQ est vrai après. C’est le formalisme central de la sémantique axiomatique.
  2. Axiome d’affectation : on “remonte” de la postcondition vers la précondition en substituant la variable : {Q[x/e]}  x:=e  {Q}\{Q[x/e]\}\; x := e \;\{Q\}.
  3. Invariant de boucle : propriété préservée à chaque itération. Trouver le bon invariant est le coeur de la preuve.
  4. Variant de boucle : quantité qui décroît strictement à chaque itération, garantissant la terminaison.
  5. Correction partielle vs totale : {P}  C  {Q}\{P\}\; C \;\{Q\} ne garantit pas la terminaison ; [P]  C  [Q][P]\; C \;[Q] la garantit.
  6. Règle de conséquence : permet de renforcer les préconditions et d’affaiblir les postconditions.
  7. Pour BP3 : la sémantique axiomatique est la moins directe (pas de boucles classiques, stochasticité), mais les concepts d’invariant et de terminaison restent pertinents pour raisonner sur les grammaires.

Pour aller plus loin

  • Article fondateur : Hoare, C.A.R. (1969). “An Axiomatic Basis for Computer Programming”. Communications of the ACM, 12(10), 576-580.
  • Le calcul wp : Dijkstra, E.W. (1976). A Discipline of Programming. Prentice-Hall. — systématise les plus faibles préconditions.
  • Manuel de référence : Winskel, G. (1993). The Formal Semantics of Programming Languages, chapitre 6. MIT Press. — traitement rigoureux de la logique de Hoare.
  • Introduction accessible : Nielson, H.R. & Nielson, F. (1992). Semantics with Applications: A Formal Introduction, chapitre 6. Wiley. — présentation pédagogique progressive.
  • Dans cette série : L6 pour la sémantique opérationnelle, L7 pour l’approche fonctionnelle.

Glossaire

  • Affaiblissement : remplacement d’une postcondition par une condition plus faible (moins précise mais toujours correcte). Si x=6x = 6 est vrai, alors x>0x > 0 l’est aussi : on a affaibli la postcondition.
  • Assertion : propriété logique sur l’état du programme à un point donné. Les préconditions et postconditions sont des assertions.
  • Axiome : règle sans prémisse, toujours applicable. L’axiome d’affectation est le seul axiome de la logique de Hoare.
  • Correction partielle : propriété {P}  C  {Q}\{P\}\; C \;\{Q\} — si CC termine, QQ est vrai. Ne dit rien sur la terminaison.
  • Correction totale : propriété [P]  C  [Q][P]\; C \;[Q] — CC termine ET QQ est vrai. Combine correction partielle et terminaison.
  • Design by Contract : méthodologie de programmation où les fonctions sont annotées avec des préconditions, postconditions et invariants. Inventé par Bertrand Meyer (Eiffel, 1986).
  • Invariant de boucle : propriété logique vraie avant la boucle et préservée par chaque itération. Clé des preuves de correction des boucles.
  • Invariant de classe : propriété qui doit etre vraie pour tout objet d’une classe, à tout moment observable (entre les appels de méthode).
  • Logique de Hoare : système formel de triplets {P}  C  {Q}\{P\}\; C \;\{Q\} et de règles d’inférence pour prouver la correction des programmes. Introduit par C.A.R. Hoare en 1969.
  • Plus faible précondition (wp) : la condition PP la moins restrictive telle que {P}  C  {Q}\{P\}\; C \;\{Q\} soit valide. Notation : wp(C,Q)\text{wp}(C, Q). Introduit par Dijkstra (1975).
  • Postcondition : assertion décrivant ce qui est garanti après l’exécution d’une commande. Notée QQ dans {P}  C  {Q}\{P\}\; C \;\{Q\}.
  • Précondition : assertion décrivant ce qui doit etre vrai avant l’exécution d’une commande. Notée PP dans {P}  C  {Q}\{P\}\; C \;\{Q\}.
  • Renforcement : remplacement d’une précondition par une condition plus forte (plus restrictive). Si on sait que {x>0}  C  {Q}\{x > 0\}\; C \;\{Q\}, alors {x=5}  C  {Q}\{x = 5\}\; C \;\{Q\} est aussi valide car x=5x = 5 implique x>0x > 0.
  • Substitution Q[x/e]Q[x/e] : opération qui remplace chaque occurrence de la variable xx par l’expression ee dans la formule QQ. Exemple : (x>3)[x/(x+1)]=(x+1>3)=(x>2)(x > 3)[x/(x + 1)] = (x + 1 > 3) = (x > 2).
  • Triplet de Hoare : notation {P}  C  {Q}\{P\}\; C \;\{Q\} qui lie une précondition PP, une commande CC et une postcondition QQ. Se lit “si PP est vrai avant CC, et si CC termine, alors QQ est vrai après”.
  • Variant (fonction de terminaison) : expression entière positive qui décroît strictement à chaque itération d’une boucle. Son existence prouve la terminaison.
  • Vérification formelle : utilisation de méthodes mathématiques pour prouver qu’un système (logiciel ou matériel) satisfait sa spécification.
  • ∧\wedge (conjonction) : connecteur logique “et”. P∧QP \wedge Q est vrai si et seulement si PP et QQ sont tous les deux vrais.
  • ¬\lnot (négation) : connecteur logique “non”. ¬P\lnot P est vrai si et seulement si PP est faux.
  • ⟹\Longrightarrow (implication) : connecteur logique “implique”. P⟹QP \Longrightarrow Q signifie “si PP est vrai, alors QQ est vrai”.

Prérequis : L5 Temps de lecture : 12 min Tags : #semantique-axiomatique #logique-hoare #verification #preuve-programme


Article précédent : L7 Série L complétée. Pour l’application à BP3, voir : série B


← Retour à l’index