Introduction à la vérification formelle avec SPARK ── Des contrats Ada à la preuve mathématique
· Go Komura · Ada, SPARK, Vérification Formelle, GNATprove, Conception par Contrat, Haute Intégrité, Langage de Programmation, Haute Fiabilité
1. Introduction ── Au-delà de « exécuter et observer »
Le moyen le plus courant d’assurer la qualité d’un logiciel reste le test.
Test : vérifie que le programme se comporte correctement pour les entrées sélectionnées
Mais les tests ont des limites intrinsèques. Le nombre d’entrées possibles est infini, et les tests seuls ne peuvent pas démontrer la correction pour toutes.
C’est ici qu’intervient la vérification formelle (Formal Verification).
Vérification formelle : prouve mathématiquement qu'une propriété est vraie pour toutes les entrées possibles
L’article précédent, « L’attrait du langage Ada », donnait un aperçu d’Ada et abordait brièvement SPARK.
Cet article se concentre spécifiquement sur la vérification formelle pratique avec SPARK et couvre les points suivants :
Ce qu'est SPARK et son rapport avec Ada
Comment écrire des contrats (Pre/Post)
Comment les prouver avec GNATprove
Comment concevoir des invariants de boucle
Comment gérer les effets de bord avec les contrats de flux de données (Global/Depends)
Une stratégie d'adoption progressive via les niveaux de preuve (de Stone à Platinum)
Comment intégrer SPARK dans des projets réels
L’objectif est de vous faire vivre le passage du test à la preuve à travers des exemples de code concrets.
Les extraits de code de cet article sont disponibles, organisés par chapitre, dans un dépôt de référence sur GitHub.
ada-spark-formal-verification - komurasoft-blog-samples (GitHub)
2. Qu’est-ce que SPARK ── Le sous-ensemble d’Ada, porte d’entrée vers le monde de la preuve
SPARK est un sous-ensemble du langage Ada.
Le fait d’être un sous-ensemble relève d’un choix de conception délibéré.
Ada complet → très expressif, mais vérifier l'ensemble n'est pas réaliste
SPARK → restreint aux fonctionnalités vérifiables, ce qui rend la preuve réaliste
Voici les principales restrictions imposées par SPARK :
Gestion dynamique de la mémoire via des pointeurs (types access) → contrôlée par un modèle de propriété
Usage libre des gestionnaires d'exceptions → autorisé sous forme restreinte
Structures de données récursives → restreintes car difficiles à prouver
Usage libre des tâches → restreint via le profil Ravenscar
Le mot « restrictions » peut sembler contraignant, mais ces restrictions sont le prix à payer pour la prouvabilité.
Le cœur de l’outillage SPARK est GNATprove, fourni par AdaCore.
Fonctionnement de GNATprove :
1. Analyse le code Ada annoté SPARK
2. Traduit les contrats (Pre/Post) et les assertions vers le langage intermédiaire Why3
3. Tente des preuves à l'aide de solveurs automatiques tels que Z3, CVC4 et Alt-Ergo
4. Rapporte les résultats sous forme de messages sur le code source
Un point crucial : SPARK est construit au-dessus d’Ada.
Code SPARK = code Ada + annotations de contrat
Autrement dit, SPARK prolonge naturellement le développement Ada quotidien. Il n’y a pas de syntaxe entièrement nouvelle à apprendre.
3. Installer GNATprove et réaliser sa première preuve
Commençons par mettre en place un environnement pour exécuter GNATprove.
Avec Alire, on peut créer un projet prêt pour SPARK avec la commande suivante :
alr init --bin spark_demo
cd spark_demo
GNATprove étant fourni avec le compilateur GNAT, la commande gnatprove est disponible dès lors que le projet se compile avec alr build.
Comme première cible de preuve, écrivons une fonction qui retourne la valeur absolue.
package Simple_Proof with SPARK_Mode is
function Abs_Value (X : Integer) return Integer
with Post => Abs_Value'Result >= 0;
end Simple_Proof;
package body Simple_Proof with SPARK_Mode is
function Abs_Value (X : Integer) return Integer is
begin
if X < 0 then
return -X;
else
return X;
end if;
end Abs_Value;
end Simple_Proof;
Points essentiels à noter :
with SPARK_Mode s'ajoute à la fois à la spécification du package et à son corps
La condition Post déclare la propriété « le résultat est non négatif »
L'attribut 'Result permet de référencer la valeur de retour de la fonction
Exécutons maintenant GNATprove sur ce code :
gnatprove -P spark_demo.gpr
Le résultat ressemble à ceci :
SUMMARY
-------
Phase 1 of 2: generation of Global contracts ...
Phase 2 of 2: flow analysis and proof ...
simple_proof.adb:5:15: info: range check proved
simple_proof.adb:6:16: info: range check proved
simple_proof.ads:3:19: info: postcondition proved
Le message postcondition proved signifie que pour toutes les entrées Integer possibles, il a été mathématiquement prouvé que la valeur de retour est non négative.
C’est là l’expérience d’entrée dans SPARK.
4. Fondements de la conception par contrat ── Écrire Pre et Post
Les preuves SPARK commencent par l’écriture de contrats.
Les contrats sont une fonctionnalité du langage Ada 2012, mais dans SPARK, ils deviennent l’entrée de la preuve.
procedure Transfer (From, To : in out Account; Amount : Positive)
with Pre => From.Balance >= Amount,
Post => From.Balance = From.Balance'Old - Amount
and then
To.Balance = To.Balance'Old + Amount;
Principes de conception pour Pre et Post :
Pre (précondition) :
Responsabilité de l'appelant
« Si vous m'appelez en satisfaisant cette condition, je garantis la Post »
Assez faible pour être satisfiable, assez forte pour être utile
Post (postcondition) :
Responsabilité de l'implémentation
« Après l'appel, le monde est dans cet état »
L'attribut 'Old permet de référencer les valeurs antérieures à l'appel
Trop forte, l'implémentation devient trop contrainte ; trop faible, rien d'utile n'est prouvé
Erreurs fréquentes et comment les éviter :
Erreur 1 : Pre trop forte
Pre => X > 0 and X < 100 and X /= 50 and ...
→ Vérifier : l'appelant peut-il toujours satisfaire cette condition ?
→ Ne pas resserrer Pre simplement parce que les tests passent
Erreur 2 : Post trop faible
Post => True
→ Ne garantit rien. La preuve est vide de sens.
Erreur 3 : Oublier les effets de bord dans Post
Post => Result = X * 2 (mise à jour d'une variable globale oubliée)
→ Rendre les effets de bord explicites avec les contrats Global/Depends (section 9)
5. Prouver l’absence de dépassement ── Garantir la sécurité numérique
L’une des choses les plus précieuses en pratique que SPARK peut prouver est l’absence de dépassement (overflow).
Considérez ce code :
procedure Increment (X : in out Integer)
with SPARK_Mode,
Pre => X < Integer'Last,
Post => X = X'Old + 1;
Pre => X < Integer'Last est la clé. Avec ce contrat, GNATprove peut prouver que l’addition ne débordera pas.
Sans contrat, GNATprove avertit d’un dépassement possible :
medium: overflow check might fail
Il en va de même pour des calculs plus complexes :
function Average (A, B : Integer) return Integer
with SPARK_Mode,
Pre => (if A >= 0 and B >= 0 then A <= Integer'Last - B
elsif A < 0 and B < 0 then A >= Integer'First - B),
Post => (if A <= B then A <= Average'Result and Average'Result <= B
else B <= Average'Result and Average'Result <= A);
La Pre semble complexe, mais elle exprime simplement que « la somme A+B ne déborde pas », en distinguant les cas selon le signe.
Point clé :
L'essence de la preuve d'absence de dépassement consiste à déclarer,
dans Pre, que le résultat d'une addition ou d'une multiplication tient
dans les bornes du type
Cela peut sembler fastidieux, mais une fois écrit, plus jamais de bogue
de dépassement ne viendra vous tourmenter
6. Invariants de boucle ── Prouver les propriétés des boucles
L’une des parties les plus difficiles de la vérification formelle est la preuve des boucles.
Les boucles s’exécutent un nombre arbitraire de fois, si bien que les tests ne peuvent pas couvrir tous les cas. SPARK utilise pour cela les invariants de boucle (Loop Invariants).
function Sum_Of_Naturals (N : Natural) return Natural
with SPARK_Mode,
Post => Sum_Of_Naturals'Result = (N * (N + 1)) / 2;
function Sum_Of_Naturals (N : Natural) return Natural is
Result : Natural := 0;
begin
for I in 1 .. N loop
Result := Result + I;
pragma Loop_Invariant (Result = (I * (I + 1)) / 2);
end loop;
return Result;
end Sum_Of_Naturals;
Concevoir des invariants de boucle exige la démarche suivante :
1. Écrire une propriété vraie au début de chaque itération
2. En choisir une qui, après la dernière itération, implique la Post recherchée
3. L'invariant exprime le calcul partiel effectué jusqu'à ce point sous forme de formule
Voici un autre exemple : la recherche dans un tableau.
function Find (Arr : Array_Of_Integer; Target : Integer) return Natural
with SPARK_Mode,
Post => (if Find'Result = 0 then
(for all K in Arr'Range => Arr (K) /= Target)
else
Arr (Find'Result) = Target);
function Find (Arr : Array_Of_Integer; Target : Integer) return Natural is
begin
for I in Arr'Range loop
if Arr (I) = Target then
return I;
end if;
pragma Loop_Invariant
(for all K in Arr'First .. I => Arr (K) /= Target);
end loop;
return 0;
end Find;
L’invariant ici est : « dans la plage examinée jusqu’ici, Target n’existe pas ».
Principes de conception des invariants de boucle :
Exprimer sous forme de formule : « après n itérations, que peut-on affirmer ? »
Pour les boucles sur tableau, le motif habituel est « pour la plage déjà traitée, P est vraie »
Un invariant trop fort ne peut pas être prouvé ; trop faible, il n'implique pas la Post
Trouver le bon équilibre est tout l'art de la preuve de boucles
7. Pragmas d’assertion ── Propriétés partielles dans le code
En plus des contrats (Pre/Post), SPARK permet d’écrire des assertions (Assert) au cœur même du code.
procedure Divide (A, B : Integer; Q, R : out Integer)
with SPARK_Mode,
Pre => B /= 0,
Post => A = Q * B + R and R >= 0 and R < abs (B);
procedure Divide (A, B : Integer; Q, R : out Integer) is
begin
Q := A / B;
R := A rem B;
pragma Assert (A = Q * B + R);
pragma Assert (R >= 0);
pragma Assert (R < abs (B));
end Divide;
Usage des différents pragmas :
Pre/Post : Promesses à l'entrée et à la sortie du sous-programme
Assert : Une propriété qui doit être vraie à un point précis du code
Loop_Invariant : Une propriété préservée à chaque itération de boucle
Loop_Variant : Une quantité décroissante prouvant la terminaison de la boucle
Bons cas d’usage pour Assert :
Vérifier les résultats intermédiaires de calculs complexes
Vérifier l'état après une branche if/else
Confirmer des propriétés des valeurs de retour après un appel de procédure
Fournir des lemmes qui facilitent les preuves suivantes
8. Contrats de flux de données ── Global et Depends
L’une des fonctionnalités puissantes de SPARK est celle des contrats de flux de données.
Ils déclarent explicitement quelles variables globales un sous-programme lit et écrit.
package Counter_Unit with SPARK_Mode is
Count : Natural := 0;
procedure Increment
with Global => (In_Out => Count);
procedure Reset
with Global => (Output => Count);
function Get_Value return Natural
with Global => (Input => Count);
end Counter_Unit;
Les modes du contrat Global sont les suivants :
Input : lecture seule (pour les fonctions)
Output : écriture seule (pour l'initialisation)
In_Out : lecture et écriture (pour les mises à jour)
De plus, les dépendances entre variables peuvent être exprimées avec Depends :
procedure Transfer
(From, To : in out Account_Type)
with Global => (Input => Exchange_Rate),
Depends => (From =>+ (From, Exchange_Rate),
To =>+ (To, Exchange_Rate));
=>+ signifie « dépend de la valeur précédente, plus ces entrées supplémentaires ».
Avantages des contrats de flux de données :
1. « Que touche cette fonction ? » se voit d'un coup d'œil
2. Les effets de bord non désirés sont détectés à la compilation
3. Ils alimentent l'analyse de flux d'information des variables
4. Ils aident à visualiser le flux de données dans les grands systèmes
9. Niveaux de preuve ── Adoption progressive de Stone à Platinum
SPARK définit le concept de niveaux de preuve (Proof Levels).
Tenter de prouver tout le code d’un seul coup mène souvent au découragement. SPARK propose donc une stratégie d’augmentation progressive de la rigueur des preuves.
Stone (niveau 0) :
Vérification de la syntaxe des contrats et de la cohérence des types uniquement
Le premier niveau à atteindre
Bronze (niveau 1) :
Stone + preuve de l'absence de lecture de variables non initialisées
Garantie de correction de base
Silver (niveau 2) :
Bronze + preuve de l'absence d'erreurs d'exécution (dépassement de
bornes, overflow, division par zéro)
L'objectif le plus pragmatique
Gold (niveau 3) :
Silver + preuve des conditions Pre/Post
Garantie de correction fonctionnelle complète
Platinum (niveau 4) :
Gold + preuve de la non-interférence des données et du flux d'information
Pour les systèmes où la sécurité prime
Une stratégie d’adoption pratique :
Phase 1 : amener l'ensemble du projet au niveau Stone
→ Repérer les erreurs d'écriture des contrats
Phase 2 : amener les modules critiques au niveau Silver
→ Éliminer les dépassements et les accès hors bornes
→ La plupart des bogues réels sont évités à ce stade
Phase 3 : prouver la logique centrale au niveau Gold
→ Garantir mathématiquement la correction fonctionnelle
→ Prouver la correction des algorithmes
Phase 4 : passer à Platinum si des exigences de sécurité l'imposent
→ Détecter les chemins de fuite d'information
10. Un flux de preuve pratique ── Réduire les retours en arrière
Voici un flux de travail pour utiliser GNATprove au quotidien :
1. Concevoir les types et les spécifications
Décider des contraintes de plage et des invariants de type
2. Écrire les contrats (Pre/Post)
Exprimer la spécification sous forme de contrats avant l'implémentation
3. Faire compiler le code
Corriger les erreurs de compilation avec GNAT
4. Exécuter GNATprove au niveau Stone
gnatprove -P proj.gpr --level=0
5. Examiner les avertissements et affiner les contrats si nécessaire
Porter une attention particulière aux problèmes d'initialisation
6. Viser le niveau Silver
gnatprove -P proj.gpr --level=2
Éliminer toutes les erreurs d'exécution
7. Ajouter des invariants de boucle si nécessaire
Si une preuve échoue, vérifier si des invariants manquent
8. Prouver la correction fonctionnelle au niveau Gold
gnatprove -P proj.gpr --level=3
Situations « impossible à prouver » fréquentes et leur traitement :
Cas 1 : « medium: postcondition might fail »
→ Post trop forte, ou Pre trop faible
→ Ou invariants de boucle insuffisants
Cas 2 : « medium: overflow check might fail »
→ Restreindre les plages de valeurs dans Pre
→ Ou réduire la plage du type lui-même
Cas 3 : « medium: array index check might fail »
→ Utiliser un invariant de boucle pour montrer que la plage de la boucle
reste dans les bornes du tableau
→ Utiliser for I in Arr'Range pour aider la reconnaissance automatique
des bornes par SPARK
Cas 4 : « prover timeout »
→ Le solveur a expiré ; découper la preuve en invariants progressifs
→ Diviser les grandes fonctions en fonctions plus petites
11. Combiner SPARK et les tests ── Comprendre la relation de complémentarité
La vérification formelle et les tests ne s’opposent pas ; ils se complètent.
Preuves SPARK :
Prouvent des propriétés pour toutes les entrées possibles
Ne couvrent que les propriétés écrites dans les contrats
Les cas non prouvables sont confiés à la revue manuelle ou aux tests
Tests :
Vérifient le comportement réel pour les entrées sélectionnées
Peuvent révéler des hypothèses implicites non écrites dans les contrats
Peuvent vérifier l'interaction avec l'environnement d'exécution
Motifs d’utilisation combinée :
1. Garantir l'absence d'erreurs d'exécution (niveau Silver) avec SPARK
2. Vérifier la correction concrète des entrées/sorties avec des tests unitaires
3. Prouver la logique centrale au niveau Gold
4. Concentrer les tests sur les parties non prouvables
Ada dispose du framework de tests unitaires AUnit. Une combinaison pratique consiste à solidifier les types et les contrats avec SPARK, puis à tester le comportement avec AUnit.
12. Lire les résultats de GNATprove ── Interpréter les messages
La sortie de GNATprove comporte trois niveaux de sévérité :
info : preuve réussie
medium : preuve échouée (avertissement). Revue manuelle nécessaire
high : preuve échouée (erreur). Violation de contrat probable
Messages fréquents et leur signification :
« postcondition proved » la condition Post a été prouvée
« range check proved » la vérification de plage a été prouvée
« overflow check proved » l'absence de dépassement a été prouvée
« index check proved » les bornes d'indice de tableau ont été prouvées
« divide by zero check proved » l'absence de division par zéro a été prouvée
« might fail » n'a pas pu être prouvé
« cannot prove » le solveur n'a pas pu achever la preuve
« prover timeout » la preuve n'a pas abouti dans le délai imparti
Procédure à suivre face à might fail :
1. Lire le code et juger s'il peut réellement échouer
2. Si oui, corriger le code
3. S'il ne devrait pas échouer, renforcer le contrat (Pre / invariant)
4. Si la preuve reste impossible, déclarer une hypothèse avec pragma Assume
(attention : Assume est une hypothèse non prouvée, à utiliser avec prudence)
13. Intégration dans des projets réels
Voici une approche réaliste pour introduire SPARK dans un projet existant :
1. Commencer par le nouveau code
Ne pas tenter de convertir tout le code existant en SPARK d'un seul coup
Activer SPARK_Mode sur les modules nouvellement écrits
2. Viser Silver
Gold (preuve fonctionnelle complète) est idéal, mais on commence par
Silver (absence d'erreurs d'exécution)
La seule prévention des dépassements apporte déjà une valeur pratique
considérable
3. Écrire les contrats en partant des interfaces
Écrire les contrats dans la spécification du package (.ads) avant l'implémentation
Avec une spécification solide, n'importe quel développeur peut écrire
un code qui satisfait les contrats
4. Intégrer dans le CI/CD
Ajouter gnatprove au pipeline d'intégration continue
Arrêter le build (ou au moins avertir) en cas d'échec de preuve
5. Accumuler des rapports de preuve
Générer des rapports avec gnatprove --report=all
Suivre l'évolution des éléments non prouvés dans le temps
Pour les projets mêlant code C/C++ et Ada, une approche progressive est efficace :
1. Écrire les nouveaux modules en Ada/SPARK
2. Communiquer avec C/C++ via Interfaces.C
3. Migrer les structures de données critiques et la logique de vérification vers SPARK
4. Étendre progressivement la couverture SPARK
14. Limites et précautions concernant SPARK
SPARK n’est pas non plus une solution miracle. Comprendre honnêtement ses limites permet une application correcte.
Ce que SPARK peut prouver :
SPARK ne prouve que les propriétés écrites dans les contrats
Les propriétés omises (oubliées à l'écriture) ne sont pas prouvées
Exemple : prouver dans Post que le résultat est trié ne garantit pas
que les éléments d'origine sont préservés, à moins de l'écrire aussi
Limites des solveurs :
Certaines propriétés complexes dépassent les capacités des solveurs
automatiques
Dans ce cas, une preuve interactive (Coq, etc.) est nécessaire
Cela dit, la preuve automatique suffit dans la plupart des cas pratiques
Restrictions du langage :
Le code fortement basé sur des pointeurs ne peut pas être converti en SPARK
La preuve de l'allocation dynamique de mémoire est limitée
La preuve des structures de données récursives est difficile
Maîtrise des développeurs :
Écrire des contrats demande de l'entraînement
Concevoir des invariants de boucle est particulièrement long à maîtriser
Un plan de montée en compétence pour toute l'équipe est indispensable
15. Conclusion ── Faire de la preuve un geste quotidien du développement
Nous avons parcouru la pratique de la vérification formelle avec SPARK.
SPARK est un sous-ensemble d'Ada, restreint aux fonctionnalités vérifiables
Les contrats (Pre/Post) sont l'entrée des preuves. Les contrats d'Ada 2012
fonctionnent directement
GNATprove exécute des preuves automatiques et rapporte les résultats au
niveau du code source
Les invariants de boucle prouvent les propriétés des boucles
Global/Depends rendent le flux de données explicite et gèrent les effets
de bord
Les niveaux de preuve (de Stone à Platinum) permettent d'augmenter la
rigueur par étapes
Viser d'abord le niveau Silver (absence d'erreurs d'exécution) est la
recommandation pratique
Tests et preuves ne s'opposent pas — ils se complètent
On pourrait avoir l’idée reçue que « la vérification formelle est trop difficile et réservée à des projets particuliers ».
Mais la combinaison actuelle de SPARK et GNATprove a atteint le niveau suivant :
1. Écrire un contrat. C'est fondamentalement la même démarche de
conception que d'écrire des types ou des tests
2. Exécuter gnatprove. Cela ressemble à une compilation
3. Regarder les résultats et corriger le code ou le contrat
Ce cycle ne diffère en rien du flux de développement quotidien où l’on corrige du code en lisant les messages d’erreur du compilateur.
La différence est la suivante : là où un compilateur garantit que « la syntaxe est correcte », GNATprove garantit « la correction pour toutes les entrées ».
La devise d’Ada présentée dans l’article précédent — « les bogues ne sont pas des choses qu’on trouve ; les types et les contrats les rendent impossibles à écrire » — franchit une étape supplémentaire avec SPARK :
L’absence de bogue est garantie par la preuve.
Commencez par ajouter SPARK_Mode et un Post à une petite fonction, puis exécutez gnatprove.
En voyant apparaître le message vert postcondition proved, votre regard sur l’assurance qualité logicielle changera.
Références
- Collection de code de référence organisée par chapitre - komurasoft-blog-samples (GitHub)
- SPARK - AdaCore
- Introduction to SPARK - learn.adacore.com
- SPARK 2014 Reference Manual
- GNATprove User’s Guide
- Ada Reference Manual (Ada 2022)
- Alire - Ada Library Repository
- Why3 - Platform for Deductive Program Verification
- L’attrait du langage Ada ── Exprimer la conception par les types, faire fonctionner des logiciels pendant des décennies - Ce site
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Guide pratique de Process Monitor (ProcMon) — identifier en 10 minutes un « paramètre non pris en compte » ou un ACCESS DENIED
« J'ai corrigé le fichier de configuration, mais rien ne change » ; « ça marchait hier, mais ça ne démarre plus aujourd'hui » — avant de ...
Migrer une application Windows vers le Web : les cas à éviter — tableau de décision et la solution réaliste du « fractionnement »
Les demandes de migration d'applications Windows internes vers le Web se multiplient, mais pour les applications reposant sur l'intégrati...
Internationalisation des applications WinForms/WPF : resx, assemblies satellites et changement de culture en pratique
Un guide pratique pour internationaliser une application de bureau Windows : la différence entre CurrentCulture et CurrentUICulture, le f...
Le CSV n'est pas « juste du texte » ── La pratique du CSV dans les applications métier C# (encodage des caractères, compatibilité Excel, protection contre l'injection)
Ce guide fait le point, du point de vue de la pratique, sur les incidents classiques des entrées/sorties CSV dans les applications métier...
N'enfermez pas HttpClient dans un using — la communication HTTP en C# pour applications métier (modèles de création, délais d'expiration, tentatives)
Instancier HttpClient à chaque appel avec using épuise les sockets, et le rendre static l'empêche de suivre les changements de DNS — cet ...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Qu'est-ce que SPARK et quel est son rapport avec Ada ?
- SPARK est un sous-ensemble délibérément restreint d'Ada, conçu pour que les propriétés d'un programme puissent être prouvées mathématiquement. Il limite les fonctionnalités difficiles à vérifier — usage libre des pointeurs, gestion illimitée des exceptions, structures de données récursives et tâches sans restriction — en échange de la prouvabilité. Le code SPARK est du code Ada auquel s'ajoutent des annotations de contrat : il s'agit donc d'un prolongement naturel du développement Ada courant, et non d'un langage différent. L'outil GNATprove traduit les contrats vers le langage intermédiaire Why3 et tente des preuves avec des solveurs automatiques comme Z3, CVC4 et Alt-Ergo.
- En quoi la vérification formelle avec SPARK diffère-t-elle des tests ?
- Les tests vérifient que le programme se comporte correctement pour les entrées choisies ; la vérification formelle prouve mathématiquement qu'une propriété est vraie pour toutes les entrées possibles. Les deux approches se complètent plutôt qu'elles ne s'opposent : SPARK ne prouve que les propriétés écrites dans les contrats, tandis que les tests peuvent révéler des hypothèses implicites jamais formalisées et vérifier l'interaction avec l'environnement d'exécution. Une combinaison pratique consiste à garantir l'absence d'erreurs d'exécution avec SPARK, à vérifier des comportements d'entrée/sortie concrets avec des tests unitaires, et à concentrer les tests sur les parties qui ne peuvent pas être prouvées.
- Quels sont les niveaux de preuve de SPARK, et lequel viser en premier ?
- SPARK définit cinq niveaux de rigueur croissante : Stone (syntaxe des contrats et cohérence des types), Bronze (ajoute la preuve d'absence de lectures non initialisées), Silver (ajoute la preuve d'absence d'erreurs d'exécution telles que dépassements, violations de bornes et division par zéro), Gold (ajoute la preuve de correction fonctionnelle des conditions Pre/Post) et Platinum (ajoute la non-interférence des données et le flux d'information). Silver est l'objectif initial le plus pragmatique — la plupart des bogues réels sont éliminés en supprimant les erreurs d'exécution — et Gold vaut la peine d'être visé pour la logique centrale dont la correction doit être garantie.
- Qu'est-ce qu'un invariant de boucle, et pourquoi les preuves SPARK en ont-elles besoin ?
- Un invariant de boucle est une propriété vraie au début de chaque itération, exprimée avec pragma Loop_Invariant. Les boucles s'exécutent un nombre arbitraire de fois, si bien que le prouveur a besoin de l'invariant pour raisonner sur toutes les itérations à la fois : il exprime le calcul partiel accompli jusque-là sous forme de formule, et après la dernière itération il doit impliquer la postcondition recherchée. Concevoir des invariants est la compétence la plus délicate dans les preuves de boucle : un invariant trop fort ne peut pas être prouvé, tandis qu'un invariant trop faible n'implique pas la postcondition.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics