PRM, prmc Un langage de programmation et un compilateur

publicité
PRM, prmc
Un langage de programmation et un compilateur
Jean Privat
LIRMM
161 rue Ada
34392 Montpellier Cedex 5 - France
[email protected]
Résumé— Ce papier présente un langage de programmation
(PRM) et son compilateur (prmc) en prenant comme points
de vues l’interaction entre humains et ordinateurs, et la
qualité des logiciels.
Mots-clés— PRM, langage
compilation séparée.
de
programmation,
objets,
I. Introduction
Bonjour !
0110010 ?
A. Langages de programmation
Les langages de programmation (ou langages informatiques)
sont des langages artificiels permettant d’écrire des
programmes informatiques. Le code source (ou simplement
code) désigne le (( texte )) d’un programme (( rédigé )) dans
un langage de programmation.
Un langage de programmation est constitué d’une
syntaxe (le vocabulaire et la grammaire) et d’une
sémantique (les abstractions exprimables et leur signification).
Toutefois, il est nécessaire de comprendre qu’un langage de
programmation ne sert pas à discuter avec un ordinateur
mais plutôt à lui donner des ordres2 .
Idée
Fig. 1. Problème de communication ?
La boutade de la figure 1 caricature le problème
fondamental sous-jacent à de nombreuses recherches en
informatique : comment faire pour que l’humain et
l’ordinateur puissent interagir ?
Le travail que j’ai mené pendant ma thèse concerne
deux facettes de ce problème de communication dans le
cadre du développement de logiciels. Plus précisément, mon
travail se situe de part (côté humain) et d’autre (côté
machine) des langages de programmation 1 . Côté humain,
j’ai travaillé sur les traits des langages et proposé le
raffinement de classes, une extension du modèle à objets.
Côté machine, j’ai travaillé sur la compilation des langages
et proposé un schéma de compilation séparé qui permette
d’obtenir l’efficacité des compilateurs globaux tout en
conservant les avantages de la compilation séparée. Afin,
de mettre en oeuvre les résultats de ma recherche, j’ai
d’une part spécifié PRM, un langage de programmation
qui intègre le raffinement et d’autre part implémenté prmc,
un compilateur PRM efficace et séparé.
II. Quelques notions de base
Afin de comprendre le paragraphe précédent et plus
particulièrement les mots en italique, nous allons tenter
de présenter quelques notions de base. Dans un premier
temps nous parlerons de langages de programmation, puis
de la qualité des logiciels. Nous présenterons ensuite les
langages à objets, une famille de langages qui permet
d’avoir une meilleure qualité des logiciels. Nous terminerons
en présentant le mécanisme de la compilation.
1 Plus précisément encore, des langages de programmation orientés
objets, statiquement typés et en héritage multiple.
Programmeur
Code source
« Écriture »
Exécution
« Lecture »
Ordinateur
Fig. 2. Programmeur, langage et ordinateur
Si l’on reprend la métaphore de la figure 1, on peut
dire que les développeurs de logiciels informatiques (ou
programmeurs 3 ) communiquent avec les ordinateur en
utilisant des langages de programmation (figure 2). Cette
communication présente deux aspects :
Écriture. Un programmeur humain doit pouvoir exprimer
ses (( ordres )). Le langage doit donc être intelligible
par le programmeur. En général, plus le langage est
naturellement intelligible, plus celui-ci est éloigné des
instructions primitives des machines physiques.
Lecture. L’ordinateur doit pouvoir (( comprendre )) le code
source. Contrairement aux humains, les machines n’ont pas
la capacité d’(( interpréter )), elle ne peuvent que lire le code
source (( à la lettre )). Il est donc nécessaire que le langage
ne soit pas ambigu.
Il existe de nombreux langages de programmations
différents. Ils se distinguent les uns des autres par leur
syntaxe bien sûr, mais également par leurs domaines
d’utilisation ou les concepts qu’ils manipulent. La figure 3
montre deux programmes calculant la factorielle. Le
premier est écrit en Basic et le second en C4 .
2 Les langages de programmation partagent de nombreuses
caractéristiques avec les langages militaires.
3 Évitez le terme (( programmateur )) qui ne concerne généralement
que les lave-linges et l’arrosage automatique.
4 Le Basic est un langage de programmation développé en 1964
INPUT "Nombre: ", N
R = 1
FOR A = 1 TO N
R = R * A
NEXT A
PRINT N; "! = "; R
#include <stdio.h>
int main(void)
{
int a, n, r=1;
printf("Nombre: ");
scanf("%d", &n);
for(a=1; a<=n; a++)
r *= a;
printf("%d! = %d\n", n, r);
return 0;
}
Fig. 3. Exemples : le calcul de la factorielle
On peut remarquer que la structure du programme écrit
en C ressemble plus ou moins à celle du programme écrit
en Basic. C’est d’une part parce que les deux programmes
implémentent le même algorithme (ils calculent la
factorielle de la même manière), et d’autre part parce que le
Basic et le C appartiennent à une même famille de langages,
celle des langages impératifs.
B. Qualité des logiciels
Le génie logiciel est la branche de l’informatique qui
s’intéresse à la manière dont le code source d’un logiciel
est spécifié puis produit. En particulier, cette science de
l’ingénieur a identifié plusieurs facteurs qui composent la
qualité globale des logiciels produits. Parmi ces facteurs
on trouve par exemple : la conformité (le logiciel faitil ce qu’il doit faire ?), la fiabilité (se comporte-t-il
correctement dans tous les cas ?), l’efficacité (utiliset-il au mieux les ressources de la machine ? (temps,
mémoire...)), la maintenabilité (est-il facile de maintenir
le logiciel dans un état correct ? (correction de bugs,
mises-à-jour mineures...)), la souplesse (est-il facile de faire
évoluer le logiciel, de l’adapter de différente manières ?), la
réutilisabilité (est-il possible de réutiliser un morceau du
logiciel dans un autre logiciel ?), etc.
Il est important de remarquer que certains facteurs de
qualité concernent plus ou moins directement le code source
des logiciels (la réutilisabilité par exemple). Les langages de
programmation ont donc pleinement leur rôle à jouer dans
l’amélioration de la qualité des logiciels.
C. Langages orientées objet
Créés il y a plus de de vingt ans, les langages à objets
(ou langages orientés objet) sont devenus la norme dans
les processus de développement de logiciels. La raison d’un
tel succès est sans doute liée à quatre caractéristiques qui
augmentent l’expressivité du langage (i.e. les abstractions
exprimables par le programmeur sont plus nombreuses) :
– le concept d’objet permet au programmeur de désigner
et de manipuler de façon naturelle des abstractions
(des objets) d’entités du monde réel. Ainsi, une
voiture, un utilisateur, un achat sur internet ou une
vidéo en train d’être visionnée peuvent être considérés
comme des objets ;
par John George Kemeny et Thomas Eugene Kurtz à l’université de
Dartmouth afin de rendre l’utilisation de l’informatique accessible aux
étudiants. Le langage C fut développé en 1972 dans les laboratoires
Bell en même temps que le système d’exploitation Unix par Dennis
Ritchie et Ken Thompson.
– le concept de classe permet au programmeur de définir
des catégories d’objets (c’est-à-dire des objets de
même nature). Par exemple, l’objet (( la 206 de Roger
)) appartient à la classe des (( Voitures )) (on dit qu’un
objet est instance d’une classe) ;
– le mécanisme d’héritage permet au programmeur de
définir (c’est-à-dire d’exprimer) de nouvelles classes
par rapport à des classes déjà définies. Par exemple, le
programmeur disposant d’une classe (( Voiture )) peut
définir une sous-classe (( Ambulance )) comme étant une
(( Voiture )) blanche qui fait (( pin-pon )), le tout sans
avoir à exprimer que les ambulances ont un moteur et
quatre roues (puisque ces informations sont héritées
de la classe (( Voiture ))) ;
– la métaphore de l’envoi de messages (appelée aussi
liaison tardive) permet d’exprimer de façon générale
des manipulations sur les objets sans se soucier de la
classe dont ils sont instances. Ainsi, le programmeur
qui considère une voiture quelconque, peut exprimer
le fait de démonter une roue ou de couper le moteur,
ceci indépendemment de la classe dont est instance
la voiture en question (celle-ci pouvant être une
ambulance, un 4x4, un corbillard, etc.).
D. Compilation
Une fois le code source d’un programme écrit par un
développeur humain, la question qui se pose concerne
l’utilisation de ce programme sur un ordinateur. Pour
utiliser un programme, la façon la plus répandue (et la
plus efficace) consiste à compiler les fichiers informatiques
contenant le code source d’un programme. L’utilisateur
final n’a plus alors qu’a exécuter le résultat de la
compilation.
D.1 Principe de la compilation
FOR A = 1 TO 9
B=B+A
NEXT A
Compilation
01100100100
11101110110111
10110010110001
10000011010110
Programmeur
Code source
Exécutable
Ordinateur
Fig. 4. Principe de la compilation
La compilation (figure 4) consiste à traduire un
programme du langage de programmation utilisé vers
le langage machine, c’est-à-dire le seul langage que
l’ordinateur est capable d’exécuter directement. On appelle
exécutable le fichier produit par la compilation, celui-ci est
constitué d’une suite instructions primitives représentés de
façon binaire.
Une fois produit, l’exécutable n’est plus lié au code
source. Il peut être distribué : les utilisateurs finaux n’ont
plus qu’a double-cliquer sur l’exécutable qui leur a été
vendu.
Un programme particulier, appelé compilateur, est
chargé d’effectuer la compilation.5 . Celui-ci fonctionne en
5 Question : qui a compilé le compilateur ? Réponse : un autre
compilateur. Question : qui a alors compilé cet autre compilateur ?
prenant en donnée le code source d’un programme et en
produisant en sortie un exécutable.
Remarque : en général, un compilateur ne fonctionne que
pour un seul langage et pour une seule plate-forme. Ainsi,
un compilateur C ne saura pas compiler un programme
écrit en Basic. De la même manière, l’exécutable produit
par un compilateur pour microprocesseurs Intel 8086 ne
pourra pas être exécuté par une machine Apple munie d’un
microprocesseur Motorola.
D.2 Compilation efficace
Parmi les facteurs de qualité des logiciels, nous avons
la performance. Or, pour un code source donné, plusieurs
traductions vers le langage machine sont possibles,
certaines étant plus performantes que d’autres. Un
compilateur est d’autant plus efficace qu’il produit du code
machine performant.
Toutefois, plus le langage de programmation est
éloigné du langage machine, plus les techniques de
compilation permettant de produire du code efficace
deviennent difficile à trouver. Ainsi, il est malheureusement
fréquent que les créateurs de langages de programmation
prennent en compte les contraintes de compilation pour
spécifier leur langage ce qui entraı̂ne inévitablement
un appauvrissement du langage (moins d’expressivité)
voire une complexification (création de nombreux cas
particuliers).
D.3 Compilation séparée et globale
code
source
code
source
compilation
code
binaire
compilation
code
binaire
édition
de liens
source du
programme
code
binaire
exécutable
Fig. 5. La compilation séparée
La compilation est un processus complexe et, pour de
nombreuses raisons pratiques, un programme n’est jamais
compilé d’un coup. On préfère compiler un programme par
petits bouts puis produire un exécutable en liant ensemble
les petits bouts compilés : on appelle ça la compilation
séparée (voir figure 5). Les avantages d’une telle façon de
faire sont nombreux : si l’on modifie un programme, seul le
bout de code modifié a besoin d’être recompilé ; une seule
compilation d’un bout de code est nécessaire même si celuici est partagé par plusieurs programmes (cela arrive très
fréquemment) ; un bout de code compilé séparément peut
être vendu tel quel à d’autres programmeurs (le vendeur
qui le souhaite peut ainsi garder le code source secret).
S’il existe une (( compilation séparée )), c’est qu’il existe
une (( compilation globale )). Celle-ci consiste à compiler
d’un coup un programme en entier. L’avantage de la
compilation globale consiste à profiter de la connaissance
de la totalité du code source pour faire des analyses plus
profondes et ainsi tenter de produire du code machine plus
efficace. Malheureusement, la compilation globale n’offre
pas la souplesse de la compilation séparée, à tel point que
dans l’industrie, l’utilisation de compilateurs globaux est
marginale.
III. PRM, le langage
PRM, pour (( Programmation, raffinement et modules )),
est le langage de programmation que nous avons développé.
C’est un langage de programmation orienté objet et
statiquement typé. Il se place donc dans la même famille
que C++, Java, C# ou Eiffel6 (les autres langages que nous
citons ne faisant pas partie de cette famille). Toutefois,
contrairement à ces quatre langages, PRM se distingue
de deux manières : sa simplicité, puisque les concepts
manipulés par le langage, ainsi que la syntaxe qui en
découle, ont été spécifiés de façon à être le plus clair possible
(en particulier parce que nous n’avons pas pris en compte
dans la spécification du langage les éventuels problèmes de
compilation), et son expressivité, puisque celui-ci contient
entre autre des traits de langages avancés qui n’existent que
dans quelques langages voire sont complètement inédits !
La syntaxe du langage est fortement inspirée de celle
des langages à typage dynamique et des langages de script
(en particulier Ruby7 ). Ces langages sont généralement
réputés pour leur facilité d’apprentissage et d’utilisation.
Toutefois, PRM revendique Pascal comme l’un de ses
lointains ancêtres8 . Au final PRM est sans doute un très
bon langage pour débuter la programmation, d’ailleurs
malgré sa jeunesse il est déjà utilisé en tant que langage
d’apprentissage à l’IUT de Béziers.
Bien que les concepts de base du langage soient (à
dessein) peu nombreux, celui-ci en intègre certains qui sont
plus ou moins rares :
– héritage multiple (existe en Eiffel et en C++) :
les classes peuvent hériter de plusieurs classes. Cela
permet par exemple d’exprimer que les poulets sont à
la fois des oiseaux et des animaux de basse-cour.
– les types primitifs sont des classes (existe en Eiffel et
Ruby) : les autres langages traitent de façon différente
les types primitifs (comme les entiers) des classes ce
qui amène parfois à des complications inutiles pour le
programmeur.
– généricité bornée (existe en Eiffel et dans la dernière
version de Java, existe aussi de façon non bornée en
C++) : c’est un concept qui permet d’augmenter le
niveau d’abstraction du code source.
– redéfinition covariante (existe en Eiffel mais pas
exactement pour les mêmes raisons) : cela permet par
exemple d’exprimer des choses comme (( les animaux
mangent de la nourriture et les vaches mangent de
l’herbe ))9 .
– itérateurs automatiques (existe en Ruby et dans la
dernière version de Java) : cela permet de manipuler
6 Bjarne Stroustrup a développé C++ au cours des années 1980 en
tant qu’extension du langage C. Java est un langage conçu en 1995
chez Sun Microsystems par James Gosling et Patrick Naughton. C#,
créé par la société Microsoft, est un langage proche de Java. Eiffel,
développé par Bertrand Meyer à partir de 1985, est un langage qui se
concentre sur la qualité logicielle.
7 Yukihiro Matsumoto a commencé l’écriture de Ruby en 1993 avec
un objectif de cohérence et d’intelligibilité.
8 Pascal est un langage crée par Niklaus Wirth en 1970 à l’université
de Zurich. Il a été conçu pour servir à l’enseignement de la
programmation de manière rigoureuse mais simple.
9 Toute la subtilité d’une telle banalité consiste à remarquer qu’il
existe malgré tout un sophisme : les animaux mangent de la
nourriture, or les vaches sont des animaux, donc les vaches mangent de
la nourriture, or les saucisses sont de la nourriture, donc on pourrait
nourrir les vaches avec des saucisses.
IV. prmc, le compilateur
prmc, pour (( PRM Compiler )), est le compilateur que
nous sommes en train de développer pour le langage PRM.
Notre objectif est ici de mettre en œuvre les techniques de
compilation que nous avons développées (qui sont toutefois
applicables à tout langage à objets et statiquement typé).
A. Principe de compilation
Comme nous l’avons dit précédemment, plus un langage
est intelligible, plus celui-ci est éloigné du fonctionnement
de la machine, et plus celui-ci est difficile à compiler
efficacement. Comment les langages et compilateurs fontils pour résoudre ce problème ? Trois voies existent : le
langage peut faire des concessions à l’intelligibilité (c’est
le choix de C++ par exemple, choix que nous réprouvons
car nous pensons qu’un langage n’a pas à être corrompu
par des considérations bassement matérielles) ; le langage
peut faire des concessions à la performance (c’est le choix
des langages de script comme Ruby, choix honorable s’il en
est) ; le langage peut faire des concessions à la souplesse
de la compilation. Cette troisième voie consiste à ajouter
des contraintes au processus de compilation comme par
exemple utiliser un compilateur global (c’est le choix de
SmartEiffel, un compilateur pour Eiffel).
Nous proposons une quatrième voie : utiliser un schéma
de compilation séparée mais qui intègre des techniques
globales jusque là réservés au compilateurs globaux. L’idée
de base consiste à repousser jusqu’au dernier moment
l’application de ces techniques, ce dernier moment étant à
l’édition de liens (le moment où les bouts de code compilés
sont liés ensemble, voir figure 5). Toutefois, il nous a fallu
résoudre un problème de taille : lors de l’édition de liens,
le code source du programme a déjà été compilé et il n’est
plus possible de revenir sur la traduction puisque le code
source n’est plus disponible à ce moment là. La solution à
ce problème est détaillée dans [2].
B. Performances
Le compilateur prmc tient ses promesses d’efficacité
comme le prouvent les différents tests que nous avons
effectués.
11
10
9
8
Temps (s)
simplement les collections d’éléments, quelque soit le
type de la collection ou des éléments. On peut alors
exprimer simplement des choses comme (( pour chaque
lapin du clapier, donner une carotte )) ou (( pour chaque
client abonné, envoyer une facture )).
Les deux principaux concepts inédits sont l’héritage
multiple sémantique idéal et le raffinement de classes.
Les différentes spécifications de l’héritage multiple dans
les langages existants posent tellement de problèmes
(peu intelligibles, voire parfois franchement ambiguës) que
certaines documentations vont jusqu’à conseiller de ne pas
faire d’héritage multiple. Notre spécification de l’héritage
multiple se base exclusivement sur une approche naturelle
de la spécialisation (c’est-à-dire sans prise en comptes
des problématiques de compilation). Au final, l’héritage
multiple de PRM est plus simple (à comprendre et à
utiliser) et gère plus naturellement les divers conflits
d’héritage.
Le raffinement de classes [1] permet une meilleure
souplesse des logiciels. L’idée de base consiste à pouvoir
faire évoluer (raffiner ) une classe d’un programme sans
avoir à modifier le morceau de code source dans lequel est
définie la classe. Ne pas avoir à toucher un morceau de code
source existant est un réel avantage dans certains cas :
– le morceau de code source est partagé par plusieurs
programmes donc la modification de ce morceau de
code aurait un impact sur tous les programmes (ce
qui n’est pas forcément souhaité) ;
– le morceau de code source est inaccessible (on nous a
vendu, sans les sources, un bout de code déjà compilé) ;
– on veut deux versions du programme, une sans
l’évolution, une avec l’évolution. Il est donc nécessaire
de pouvoir conserver le morceau de code non modifié.
D’autres approches ont été proposées pour répondre
aux mêmes besoins (parmi lesquelles la programmation
orientée aspect). Toutefois, notre proposition a l’avantage
de s’intégrer parfaitement à notre spécification de l’héritage
multiple et permet de conserver le langage simple et
intelligible.
7
g++
smarteiffel
prmc
6
5
4
3
2
1
0
5
10
15
20
25
Nombre de réponses potentielles par envoi de message
30
35
Fig. 6. Performances de trois compilateurs
En particulier, la figure 6 compare l’implémentation de
l’envoi de message vis-à-vis de trois compilateurs : g++ (un
compilateur séparé pour C++), SmartEiffel (le compilateur
global Eiffel) et prmc. En abscisse nous faisons varier la
complexité des envois de messages (le nombre de classes
différentes des receveurs potentielles) et en ordonnée nous
notons le temps mesuré. Résultat, on remarque que prmc
est aussi efficace que le compilateur global (et même plus
efficace dans les envois de messages les plus complexes !)
V. Perspectives
Bien qu’utilisable, le langage PRM et son compilateur
prmc sont actuellement en développement. Nous espérons
obtenir une première version stable et utilisable par tout
un chacun cet été.
Pour en savoir plus (ou pourquoi pas participer à
l’élaboration), vous pouvez consulter le site web :
http://www.lirmm.fr/∼privat/prm
Références
[1] J. Privat et R. Ducournau. Raffinement de classes dans les
langages à objets statiquement typés. M. Huchard, S. Ducasse,
et O. Nierstrasz, editors, Actes LMO’05 in L’Objet vol. 11, pages
17–32. Hermès, 2005.
[2] J. Privat et R. Ducournau. Link-time static analysis for efficient
separate compilation of object-oriented languages. M. Ernst et
T. Jensen, editors, Program Analysis for Software Tools and
Engineering, pages 29–36, 2005.
Téléchargement