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.