Projet Lande

previous up next contents
Précédent : Présentation générale et objectifs Remonter : Projet LANDE, Conception et validation Suivant : Grands domaines d'application



Fondements scientifiques

 

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.

Sémantique des langages de programmation

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:

$ E ~::=~N ~\mid~ E_1 ~+~ E_2~~~~~~~~$


$N$ représente un entier

Sémantiques dénotationnelles

 

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 $\times$ Nat $\rightarrow$ Nat, la sémantique dénotationnelle de notre langage se décrit comme suit:


\begin{tabular}{lcl} {\Large $\varepsilon$} & : & Expression $\rightarrow$\sp... ... {[\![E_1 ]\!]},$\space {\Large $\varepsilon$}${[\![E_2 ]\!]})$\\ \end{tabular}



Dans la deuxième ligne de cet exemple, il est important de noter la distinction entre le symbole $N$, qui dénote un élément de syntaxe du langage, et $Val(N)$ qui représente la valeur correspondant à $N$ dans l'ensemble Nat. Sur ce langage élémentaire, la sémantique dénotationnelle apparaît presque comme une paraphrase de la syntaxe. Ce n'est plus le cas pour des langages plus réalistes. Par exemple, la sémantique d'un langage impératif classique encode à l'aide de fonctions un environnement, une mémoire et le flot de contrôle; la sémantique d'un programme récursif est la plus petite solution de l'équation qui le définit (plus petit point fixe).

Sémantiques opérationnelles

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:

$ {{{N_1}+{N_2}}\Rightarrow {N}} $$N$ est la somme de $N_1$ et $N_2$



$ \mbox{$\frac{\begin{array}{@{}l@{}}\rule{0pt}{0pt}{E_1}\Rightarrow {E_1'}\end... ...y}}$\settowidth {\condlength}{} \ifdim\condlength\gtpt\hspace{1em}\fi\ \ } $ $~~~~~ \mbox{$\frac{\begin{array}{@{}l@{}}\rule{0pt}{0pt}{E_2}\Rightarrow {E_2'... ...y}}$\settowidth {\condlength}{} \ifdim\condlength\gtpt\hspace{1em}\fi\ \ } $


Une règle d'inférence est constituée d'hypothèses (partie haute) et de conclusions (partie basse). Dans cet exemple, $N$ dénote une expression complètement réduite (c'est à dire un entier) et $E_i$ 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.

$ {{N}\Rightarrow {N}}{}~~~~~~~~~~~~~~~$ $ \mbox{$\frac{\begin{array}{@{}l@{}}\rule{0pt}{0pt}{E_1}\Rightarrow {N_1} \hsp... ...t\hspace{1em}\fioù $N$\space est la somme de ${N_1}$\space et ${N_2}$\ \ } $

Contrairement à la SOS précédente, cette sémantique n'impose pas d'ordre d'évaluation particulier entre $E_1$ et $E_2$. 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.

Analyse de programmes

  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 $x = f(x)$$f$ 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 $f^n(\bot)$$\bot$ 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.

Débogage

  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:

1.
La première tâche consiste à déterminer les informations qui doivent apparaître dans la trace. La trace est calquée sur la sémantique opérationnelle du langage sans forcément en donner tous les détails. Elle fournit une abstraction des étapes de calcul dont l'objectif est la compréhension par l'utilisateur du comportement des programmes. Elle dépend donc du langage et du type d'utilisateur potentiel.
2.
La deuxième tâche est la mise en oeuvre des traceurs qui nécessite l'insertion d'instructions de trace dans les mécanismes d'exécution des programmes (appelée instrumentation dans la suite). Cette instrumentation peut se faire à différents niveaux: dans le code source, dans le compilateur ou dans l'émulateur quand il en existe un. La pratique courante consiste à instrumenter à un niveau bas [Ros96] mais plus l'instrumentation est faite à un niveau haut, plus elle est portable.
3.
Quand le programmeur dispose d'un traceur, il lui reste à analyser les traces pour comprendre les comportements des programmes et localiser les fautes. Cependant ces traces donnent souvent trop de détails par rapport à la panne analysée. La troisième tâche consiste à automatiser le filtrage et l'analyse des traces d'exécution afin de donner des informations plus pertinentes au programmeur qui peut ainsi se concentrer sur son processus cognitif.

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.

Test de logiciels

  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) [XMd$^{+}$94]. 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 [XMd$^{+}$94]. 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é.

Langages déclaratifs

  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, $\lambda$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 $\lambda$-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.



previous up next contents Précédent : Présentation générale et objectifs Remonter : Projet LANDE, Conception et validation Suivant : Grands domaines d'application