Précédent : Présentation générale et
objectifs Remonter : Projet LANDE, Conception et validation
Suivant : Grands domaines d'application
Une caractéristique importante des méthodes proposées dans le projet LANDE est de reposer sur des bases formelles. S'agissant des langages de programmation, ces bases peuvent être fournies de différentes manières par ce qu'on appelle des sémantiques. Ces sémantiques sont ensuite utilisées pour définir des analyses de programmes qui permettent d'extraire des informations à partir du code des programmes (analyse statique) ou d'une trace d'exécution (analyse dynamique). Les analyses peuvent avoir différentes applications et celles qui intéressent au premier chef le projet LANDE sont l'aide à la mise au point ou débogage de programmes et le test de logiciels. Ces applications ne concernent pas un langage de programmation spécifique mais la validation des programmes peut être notoirement simplifiée si on peut imposer une discipline de programmation a priori. Les langages de haut niveau, en particulier les langages déclaratifs peuvent être vus comme un moyen d'introduire une telle discipline.
Mots-clés : Sémantique, Sémantique dénotationnelle,
Sémantique opérationnelle
Résumé : La sémantique d'un langage de programmation s'attache à donner un sens aux programmes. Il existe différentes méthodes formelles de définition de sémantique comme les méthodes opérationnelle et dénotationnelle. Une sémantique dénotationnelle attribue un sens aux programmes d'un langage en associant à chaque construction syntaxique du langage une valeur dans un domaine de définition. Une sémantique opérationnelle donne un sens aux programmes en terme d'étapes de calcul (ou réécritures). Quel que soit son mode de définition, une sémantique permet d'ôter toute ambiguïté dans la définition d'un langage de programmation. Elle peut aussi fournir une base pour des techniques de manipulation formelle de programmes: preuves de propriétés de correction, analyse, transformation. C'est dans cette optique que les sémantiques de langages sont utilisées dans le projet LANDE.
La sémantique d'un langage de programmation s'attache à donner
un sens aux programmes. Il existe différentes méthodes formelles
de définition de sémantique comme les méthodes axiomatique,
algébrique, opérationnelle ou dénotationnelle. Nous présentons
ici les méthodes dénotationnelle et opérationnelle sur un langage
très simple d'expressions arithmétiques:
Une sémantique dénotationnelle [Sch86] attribue un sens aux programmes d'un langage à l'aide d'une fonction qui associe à chaque construction syntaxique du langage une valeur dans un domaine de définition. La sémantique d'une expression est construite à partir de celle de ses sous-expressions; on dit que la sémantique est compositionnelle. La technique de preuve classique quand on travaille avec de telles sémantiques est la récurrence sur la structure (structural induction).
En prenant les entiers naturels Nat comme domaine
sémantique et la fonction Plus: Nat
Nat
Nat, la sémantique
dénotationnelle de notre langage se décrit comme suit:
Les sémantiques opérationnelles donnent un sens aux programmes en terme d'étapes de calcul (ou réécritures). Nous présentons ici deux styles de sémantiques opérationnelles: les sémantiques opérationnelles structurelles et les sémantiques naturelles.
Une sémantique opérationnelle structurelle (SOS) [NN92] est un système composé d'axiomes et de règles d'inférence qui décrit le comportement du programme en terme d'étapes élémentaires de calcul (on parle de sémantique à petits pas). La technique de preuve classique associée à ce type de sémantique est la récurrence sur le nombre d'étapes de calcul.
La SOS de notre langage se décrit à l'aide d'un axiome et de deux règles d'inférence:
Une règle d'inférence est constituée d'hypothèses (partie
haute) et de conclusions (partie basse). Dans cet exemple,
dénote une expression complètement réduite (c'est à
dire un entier) et
des expressions quelconques. La
seconde règle ne peut donc s'appliquer que si l'expression à
gauche du symbole
a déjà été calculée, ce qui impose un
ordre d'évaluation des arguments
<<gauche-droite>>.
Une sémantique naturelle [Kah87] décrit le comportement du programme par un arbre de dérivation décrivant le calcul de ses composants. Elle ne fait apparaître que les réductions des expressions en leur résultat final (leur forme normale). On parle de sémantique à grands pas et la technique de preuve associée est la récurrence sur les arbres de dérivation.
La sémantique naturelle de notre langage se décrit comme suit.
Contrairement à la SOS précédente,
cette sémantique n'impose pas d'ordre d'évaluation particulier
entre et
. Les sémantiques naturelles permettent
de cumuler certains avantages des SOS et
des sémantiques dénotationnelles: comme les premières, elles
fournissent des informations sur les étapes de calcul, ce qui
facilite la définition d'un certain nombre d'analyses; comme les
secondes, elles déterminent le sens d'une expression en fonction
de ceux de ses sous-expressions. Cette forme de compositionnalité
facilite les raisonnements sur les programmes.
Quel que soit son mode de définition, une sémantique permet d'ôter toute ambiguïté dans la définition d'un langage de programmation. Elle peut aussi fournir une base pour des techniques de manipulation formelle de programmes: preuves de propriétés de correction, analyse, transformation. C'est dans cette optique que les sémantiques de langages sont utilisées dans le projet LANDE.
Mots-clés : Analyse
dynamique, Analyse statique, Sémantique, Interprétation
abstraite, compilation optimisante
Interprétation abstraite: L'interprétation abstraite
est un cadre permettant de relier différentes interprétations
sémantiques d'un programme. Souvent, l'interprétation abstraite
sert à montrer la correction d'une analyse, présentée comme une
définition de la sémantique d'un langage sur un ensemble de
propriétés <<abstraites>>, par rapport à la
sémantique standard du langage.
Itération de points fixes: Le résultat d'une analyse
est souvent donné comme la solution d'une équation où
est une fonction monotone sur un ordre partiel.
Le théorème de Knaster-Tarski indique un algorithme pour trouver
un tel point fixe en calculant la limite de la suite itérative
où
désigne l'élément le plus petit dans
l'ordre partiel.
Résumé : L'analyse de programmes désigne l'ensemble des techniques qui permettent de déduire mécaniquement des propriétés des programmes. Ses principaux domaines d'application sont la compilation et l'aide à la mise au point de programmes. Comme exemples d'analyses existantes, nous pouvons citer l'analyse d'alias, le slicing, les analyses de dépendances. Une analyse peut être dynamique, elle porte alors sur une trace d'exécution particulière, ou statique, et valable pour toute exécution de programme. Dans les deux cas, sa correction doit être assurée, ce qui signifie que les informations qu'elle procure doivent être cohérentes avec la sémantique du programme analysé.
L'analyse de programmes désigne l'ensemble des techniques qui
permettent de déduire mécaniquement des propriétés des
programmes. Ses principaux domaines d'application sont la
compilation et l'aide à la mise au point de programmes.
On distingue deux classes d'analyses: les analyses dynamiques et les analyses statiques. L'analyse dynamique déduit des propriétés d'un programme à partir d'une trace d'exécution particulière [BGL93]. En revanche, l'analyse statique [AH87] permet d'établir des propriétés satisfaites par un programme pour toutes ses exécutions. L'information recherchée est en général incalculable ou d'une complexité importante. Une analyse statique ne peut donc calculer qu'une approximation de la solution idéale. En conséquence, les résultats de l'analyse statique sont moins précis mais plus généraux que ceux fournis par une analyse dynamique.
La conception d'une analyse comprend deux phases: la
spécification et l'implantation. L'analyse doit être spécifiée
d'une manière qui permet de prouver sa correction; celle-ci
garantit la cohérence du résultat de l'analyse par rapport à la
sémantique du langage (cf module ). La correction et la précision
des analyses ont été étudiées de manière extensive dans le cadre
de l'interprétation abstraite [Cou97]. Le résultat de cette
première phase de conception d'analyse est souvent un système
d'équations récursives dont la solution décrit les propriétés
recherchées. On dispose d'algorithmes itératifs pour résoudre ce
système d'équations (<<itérateurs de points fixes>>).
On peut également s'appuyer sur des calculs formels sur l'algèbre
des propriétés étudiées (calcul symbolique) afin d'améliorer
l'efficacité de la résolution.
Mots-clés :
Environnement de programmation, Analyse de programme,
Sémantique
Erreur: Une erreur est une action humaine qui fait
qu'un résultat incorrect est produit par un programme. Par
exemple, une erreur peut être d'intervertir deux variables A et
B.
Faute: Une faute est une étape, un processus ou une
définition de données erronés dans un programme. Une erreur peut
générer une ou plusieurs fautes. Par exemple, une faute induite
par l'erreur citée plus haut peut être qu'un test d'arrêt d'une
boucle se fait sur A qui n'est pas mise à jour.
Panne: Une panne est l'incapacité d'un programme à
effectuer ses fonctionnalités requises. Une faute peut générer
une ou plusieurs pannes [ANS]. Un exemple de panne résultant
de la faute citée plus haut est que le programme ne termine
pas.
Résumé : Le débogage consiste à localiser et corriger les fautes qui sont responsables des pannes logicielles. Le débogage est une activité cognitive complexe qui nécessite, en général, de remonter jusqu'à l'erreur humaine pour comprendre les raisons des fautes qui ont engendré les pannes.
Il existe des outils, communément appelés débogueurs, qui aident le programmeur à identifier les comportements non-attendus du programme. Ces outils donnent une image (appelée trace) des détails de l'exécution des programmes. On peut identifier trois tâches principales pour la réalisation d'un véritable débogueur. La première tâche consiste à déterminer les informations qui doivent apparaître dans la trace. La deuxième tâche est la mise en oeuvre des traceurs. La troisième tâche consiste à automatiser le filtrage et l'analyse des traces d'exécution afin de donner des informations pertinentes au programmeur qui peut ainsi se concentrer sur le processus cognitif.
Le débogage consiste à localiser et corriger les fautes qui
sont responsables des pannes logicielles. Une panne peut être
détectée après une exécution, par exemple à la suite de phases de
test (cf module ) ou lors d'une phase de
vérification formelle. La première situation, la plus fréquente
dans la pratique actuelle, correspond à ce qu'on appelle le
débogage dynamique; la seconde sera qualifiée de
débogage statique. Dans les deux cas, l'objectif visé est
de faire cesser les pannes identifiées.
Le débogage est une activité cognitive complexe qui nécessite, en général, de remonter jusqu'à l'erreur humaine pour comprendre les raisons des fautes qui ont engendré les pannes. Une panne est un symptôme de faute qui se manifeste en un comportement erroné du programme. Bien souvent le programmeur ne maîtrise pas toutes les facettes du comportement d'un programme. Par exemple, des points de sémantique opérationnelle du langage peuvent lui échapper, le programme peut être trop complexe, ou les librairies utilisées peuvent avoir une documentation obscure. Nous présentons dans un premier temps la problématique du débogage dynamique avant de résumer les particularités introduites par le débogage statique.
Pour ce qui est du débogage dynamique, il existe des outils, communément appelés débogueurs, qui aident le programmeur à identifier les comportements du programme qui ne correspondent pas à l'idée qu'il s'en faisait. Ces outils, qui devraient plutôt s'appeler traceurs, donnent une image (appelée trace) des détails de l'exécution des programmes. Une trace est composée d'événements remarquables.
On peut identifier trois tâches principales pour la réalisation d'un véritable débogueur dynamique:
La dichotomie débogueur statique / débogueur dynamique reflète
tout à fait la distinction introduite plus haut (module ) entre analyse statique et analyse
dynamique. De fait, un débogueur statique peut être vu comme un
analyseur statique dédié à la vérification de certaines classes
de propriétés et intégré dans un outil interactif. L'interaction
doit permettre à l'utilisateur de vérifier certaines hypothèses
sur le comportement du programme et d'identifier d'éventuelles
causes de dysfonctionnement sans exécuter le programme. Les trois
tâches identifiées plus haut pour le débogage dynamique se
retrouvent mutatis mutandis dans le contexte du débogage
statique: les traces sont alors des abstractions de la sémantique
opérationnelle du langage (cf module
) et le filtrage réalise une
approximation permettant de rendre décidable la propriété
recherchée.
Mots-clés :
Critère de test, Hypothèse de test, Test unitaire, Test
d'intégration, Test fonctionnel, Test système, Test en boîte
noire, Test en boîte blanche, Test structurel
Jeu de test: Un jeu de test est un ensemble de données
de test.
Critère de test: Un critère permet de spécifier
formellement un objectif (informel) de test. Un critère de test
peut, par exemple, indiquer le parcours de toutes les branches
d'un programme, ou l'examen de certains sous-domaines d'une
opération.
Validité: Un critère de test est dit valide si pour tout
programme incorrect, il existe un jeu de test non réussi
satisfaisant le critère.
Fiabilité: Un critère est dit fiable s'il produit
uniquement des jeux de test réussis ou des jeux de test non
réussis. Les jeux de test satisfaisant un critère fiable sont
donc équivalents du point de vue du test.
Complétude: Un critère est dit complet pour un programme
s'il produit uniquement des jeux de test qui suffisent à
déterminer la correction du programme (pour lesquels tout
programme passant le jeu de test avec succès est correct)
[XMd94]. Tout
critère valide et fiable est complet.
Hypothèse de test: La complétude étant hors d'atteinte en
général, on peut qualifier un jeu de test par des hypothèses de
test qui caractérisent les propriétés qu'un programme doit
satisfaire pour que la réussite du test entraîne sa
correction
Résumé : Le test comporte une grande variété de tâches qui comprend notamment la conception des jeux de test, leur instrumentation, leur exécution, le dépouillement des résultats et la sélection des tests de non-régression (en cas de modification des programmes). La plupart de ces étapes repose sur l'empirisme et l'aide fournie par les outils actuels reste insuffisante. Cependant, certaines de ces tâches peuvent être systématisées et même, dans une certaine mesure, mécanisées. La génération de jeux de test en fait partie et sa systématisation constitue l'objectif majeur des activités du projet LANDE sur ce thème. Les principales difficultés à résoudre concernent la formalisation des critères de test et l'analyse des documents d'entrée (spécification ou code source) pour engendrer des données constituant un jeu de test satisfaisant. Le bénéfice d'une telle systématisation est double: d'une part les jeux de test ainsi produits sont de meilleure qualité que ceux que peut inventer un testeur (et justifiés par rapport à un critère précis); d'autre part, la possibilité de mécaniser le procédé (au moins partiellement) apporte des gains significatifs en terme de productivité.
On distingue généralement quatre types de tests, chacun étant lié à l'une des phases de conception des logiciels. Les premiers tests soumis au logiciel ont pour cible les composants élémentaires de l'application à tester. Pour cette raison, ils sont appelés test unitaires (on trouve aussi le terme test de composant). La seconde phase de test, les tests d'intégration, correspond à la phase d'intégration progressive des différents composants élémentaires qui ont déjà passé avec succès l'épreuve des tests unitaires. L'objectif est de mettre en évidence les dysfonctionnements engendrés par leur assemblage. Les tests fonctionnels sont ensuite exécutés sur l'application dont tous les composants ont été assemblés et intégrés. Le dernier type de test s'applique à la version complète de l'application déployée dans son environnement d'exécution. Ces tests, que l'on nomme tests système, consistent à détecter des fautes ou des comportements incorrects de l'ensemble du système en situation réelle. Les tests de recette sont des tests système.
Pour concevoir ces différents types de test, il existe un
ensemble de techniques qui se décompose en deux familles
[XMd94]. La
première famille réunit les techniques de test dites en boîte
noire qui reposent sur une spécification (informelle,
semi-formelle ou formelle) du programme. Le code du programme est
considéré inaccessible et n'est pas utilisé pour sélectionner les
données de test. Les tests produits sont dits
fonctionnels.
La seconde famille est constituée des techniques de test dites en boîte blanche qui s'appuient exclusivement sur des analyses du code de l'application [Bei90]. Ces techniques reposent sur l'examen de la structure du programme et le calcul de flots de contrôle ou de données. Les tests produits sont dits structurels.
Le test comporte une grande variété de tâches qui comprend notamment la conception des jeux de test, leur instrumentation, leur exécution, le dépouillement des résultats et la sélection des tests de non-régression (en cas de modification des programmes). La plupart de ces étapes repose sur l'empirisme et l'aide fournie par les outils actuels reste insuffisante. Cependant, certaines de ces tâches peuvent être systématisées et même, dans une certaine mesure, mécanisées. La génération de jeux de test en fait partie et sa systématisation constitue l'objectif majeur des activités du projet LANDE sur ce thème. Les principales difficultés à résoudre concernent la formalisation des critères de test et l'analyse des documents d'entrée (spécification ou code source) pour engendrer des données constituant un jeu de test satisfaisant. Le bénéfice d'une telle systématisation est double: d'une part les jeux de test ainsi produits sont de meilleure qualité que ceux que peut inventer un testeur (et justifiés par rapport à un critère précis); d'autre part, la possibilité de mécaniser le procédé (au moins partiellement) apporte des gains significatifs en terme de productivité.
Mots-clés :
Langages fonctionnels, Langages de programmation logique,
Correction, Efficacité, Évolutivité, Maintenance
Résumé : Les langages de programmation déclaratifs sont fondés sur la déclaration du résultat à atteindre plutôt que du moyen de l'atteindre. Leur mise en oeuvre exige un effort spécifique pour passer automatiquement d'une définition de nature déclarative à une version opérationnelle efficace. En contrepartie, ces langages sont adaptés à l'usage de méthodes formelles (analyse de programmes, vérification). Les langages déclaratifs étudiés dans le projet LANDE appartiennent soit à la famille de la programmation fonctionnelle, soit à celle de la programmation logique.
Les langages de programmation forment des familles qui incarnent des disciplines de programmation. La famille des langages de programmation déclaratifs comprend les langages qui sont fondés sur la déclaration du résultat à atteindre plutôt que du moyen de l'atteindre. La discipline mise en oeuvre dans ces langages consiste à s'engager le moins possible dans des détails opérationnels afin de diminuer le fossé entre ce que souhaite le programmeur et ce que le langage de programmation permet d'exprimer.
Le projet LANDE s'intéresse à deux
espèces de langages de programmation déclaratifs qui sont les
langages fonctionnels (Lisp, ML, Haskell, etc.) et les langages
logiques (Prolog, Prolog, Mercury, etc.). Une
remarque importante à faire à leur sujet est que ces langages
utilisent des formalismes qui ont présidé à la formalisation de
la notion de calcul: le
-calcul [Ros84] et le calcul
des prédicats. Dans les deux cas, les programmes sont des
formules mais elles sont interprétées différemment.
L'opération essentielle des langages fonctionnels est la
réduction qui permet de remplacer une formule par une
autre formule équivalente, mais plus «simple», jusqu'à obtenir
une formule qui n'est plus réductible, et que l'on appelle une
forme normale. On convient que cette forme normale est le
résultat du calcul. L'opération essentielle des langages logiques
est la déduction. On l'emploie pour construire des
preuves, et on convient que le résultat du calcul est extrait de
ces preuves. Il s'agit le plus souvent des valeurs données dans
les preuves à certaines variables. Pour autant que la
correspondance de Curry-Howard s'applique (langages fonctionnels
typés), la preuve est l'objet commun à ces deux familles de
langages de programmation; les langages fonctionnels les
normalisent, et les langages logiques les construisent.
L'intérêt premier des langages de programmation déclaratifs et qu'ils se prêtent aux manipulations formelles. La raison majeure est l'absence d'effets de bord dans ces langages: les entités de base (fonctions ou prédicats) peuvent ainsi être manipulées directement comme des objets mathématiques.
Les enjeux des langages fonctionnels et logiques sont assez similaires. D'une part, il faut réussir à mettre en oeuvre efficacement les calculs décrits dans ces langages. D'autre part, il faut concevoir les outils de programmation qui accompagnent ces langages.
Un autre formalisme déclaratif traité dans le projet LANDE est celui des bases de données déductives. Il partage les mêmes fondements que la programmation logique mais à des fins différentes. Ici, l'enjeu est la description de grands volumes de données, des lois qui structurent ces données et des requêtes des utilisateurs. La complétude calculatoire n'est plus recherchée. Au contraire, on veut que le problème de répondre à une requête soit décidable.