Compilation et interprétation
Avec un peu de Java à la fin.


Grace Hopper predra sa retraite en 1986
avec le grade de Rear admiral
Si les algorithmes sous forme de pseudo-code ou d'organigrammes de programmation sont d'une grande utilité pour ceux qui doivent réaliser des programmes informatiques, un autre outil va grandement leur faciliter la vie (selon le point de vue de Bob) ou les rendre plus productifs (selon le point de vue d'Alice).

Il s'agit des compilateurs. On en a déjà parlé à propos de Grace Hopper qui a réalisé le premier compilateur fonctionnel sur la machine UNIVAC 1 en 1951, compilateur qu'elle a appelé "A-0 System".

À cette époque, l'écriture des programmes était très proche du langage machine, ce qui fait que ce compilateur était en fait un simple chargeur de sous-programmes.

Son rôle essentiel était d'ajouter le code correspondant à ces sous-programmes préexistants (ou routines) qui étaient identifiés par de simples références dans le programme principal.

C'est ce qu'on appellera par la suite un "éditeur de liens". Malgré tout, le mécanisme de la compilation était dorénavant en place et il n'a pas cessé d'évoluer depuis.

Ce mécanisme à surtout permis de créer des langages plus éloignés du langage des machines et plus proche du langage des humains, le travail de traduction étant dévolu au compilateur. Voilà une bonne nouvelle, non ? Mais, voyons plutôt comment fonctionne un compilateur actuel.
Ce code objet compilé à partir d'un code source en langage C
affiche les mots hello world.


Supposons un morceau de code tout bête dans un langage imaginaire :

   // Une simple addition
     x := 2;
     y := 3;
     z := x + y;
     print "la somme de x + y est ", z;
   // fin du programme


Ce sera notre "programme source". Le travail du compilateur va être de créer à partir de là un "programme objet", c'est-à-dire un fichier exécutable par l'ordinateur. Ce programme, finalement, ressemblera à ça :

0011100101001001000111001000100100010001
1110010100100100011100100010010001000100
01011101110010101010101110111001010101...


Ne cherchez pas à décoder, pour notre exemple les 0 et les 1 sont complètement aléatoires. Pour un code objet réel, voyez plutôt la figure 1 où l'affichage est en hexadécimal pour gagner de la place.

Revenons à notre compilateur et à sa mission : créer un programme objet. Pour faire cela, il va procéder par étapes successives :

Tout d'abord il va procéder à l'élimination de tout ce qui n'est pas signifiant comme les espaces inutiles et les commentaires et créer des "unités lexicales" (par exemple (variable)x) pour que le code reste compréhensible. Cette phase s'appelle l'analyse lexicale. On peut représenter le résultat de cette étape de la façon suivante (en réalité, le codage est plus concis et il n'y a pas de retour à la ligne) :

(variable)x(opérateur):=(valeur)2(variable)y(opérateur):=(valeur)3
(séparateur);(variable)z(opérateur):=(variable)x(opérateur)+(variable)y
(séparateur);(mot-clé)print(séparateur)"(littéral)la somme de x + y est
(séparateur)"(séparateur),(variable)z(séparateur);


Ensuite, le compilateur va analyser ce code pour en "comprendre" les structures et les mettre en évidence en les "décorant" c'est-à-dire en y adjoignant les informations complémentaires qui lui sont nécessaires pour la traduction en langage machine. Comme on l'a dit au début, les structures des langages évolués se sont éloignées des structures du langage machine pour être plus compréhensibles par les humains, le compilateur va donc devoir les adapter. Cette phase s'appelle l'analyse syntaxique.

Schéma de la chaîne de compilation d'un code informatique.

Après cette phase, il va s'assurer que le programme respecte les règles du langage et va construire une "table des symboles" où figureront entre autres des informations comme le type et l'emplacement mémoire des données. C'est surtout au cours de cette phase, appelée analyse sémantique, que le compilateur va signaler les bugs éventuels.

Enfin, le compilateur va générer le code de notre "programme objet" exécutable en cherchant à l'optimiser (rapidité d'exécution et encombrement mémoire) tout en incorporant les liens éventuellement nécessaires vers les sous-programmes externes ou bien en intégrant directement ces derniers dans le code.

Dans la jungle des microprocesseurs...

Maintenant, il est temps d'avouer une chose terrible : Ce programme ne va fonctionner que sur un certain type de machine, pas sur toutes !

En effet, le code machine est différent d'une famille de microprocesseurs à une autre. Si votre code source utilise des ressources nouvelles qui viennent d'être intégrées dans une famille de microprocesseurs, votre application ne fonctionnera que pour cette famille.

Dans la jungle des systèmes...

Et ce n'est pas tout. Si le compilateur, dans sa phase d'édition des liens, a prévu que l'application doit charger une routine (un fichier .dll par exemple) "à la volée" sur la machine de l'utilisateur il va falloir que cette routine soit présente ! Donc, non seulement votre programme s'adresse à une famille de microprocesseurs (Intel 80386, AMD K7...), mais en plus, il s'adresse à un système d'exploitation particulier (Windows, Linux, Androïd, MacOS...)

À moins de ne cibler que le marché d'un certain type de machine ou de créer des programmes sur d'anciens standards restés compatibles, ce qui peut parfois se faire, nous voilà dans de beaux draps...

Rassurez-vous, dans une telle situation, on peut s'en sortir en utilisant un compilateur différent pour chaque type de machines. C'est la solution choisie par les créateurs du langage COBOL à la fin des années 1950. Ce langage orienté gestion s'adressait au monde des affaires et les solutions matérielles commençaient à se diversifier. Pour atteindre un plus vaste public sans avoir à réécrire à chaque fois les programmes, ses promoteurs ont donc eu l'idée de créer des compilateurs adaptés à chaque type de machine.

IBM 704
Une autre solution consiste à reporter la phase de compilation sur la machine de l'utilisateur. Le programme source devient un simple script, et c'est un interpréteur installé sur la machine de l'utilisateur qui va le compiler à la volée en vue de son exécution.

L'interpréteur lit une instruction et, si elle est correcte, il la traduit en langage machine, l'exécute et passe à la suivante.

Cette façon de faire apporte plus de facilités pour la programmation et permet une plus grande "portabilité". Un même code source pourra directement fonctionner sur des machines différentes, dès l'instant qu'elles sont équipées du bon interpréteur.

Le logo de Perl
Seul petit inconvénient, l'exécution sera plus lente, ce qui pourra être rédhibitoire dans certains cas. Le premier langage qui utilisera cette méthode sera le LISP créé à la fin des annèes 1950 par John McCarthy pour tourner sur un IBM 704. De nombreux langages interprétés suivront, BASIC, JavaScript, PHP, Perl, etc.

Le logo de Java
Il existe encore une autre façon de procéder assez rusée : Développer des applications pour une machine virtuelle qui pourra être "émulée" sur une large gamme de machines. Il suffira alors de développer une seule version de ces applications et de distribuer un émulateur pour chaque type d'ordinateur. C'est ce que fait la technologie Java avec sa machine virtuelle JRE (Java Runtime Environment).

Compilateur, interpréteur, machine virtuelle, de nos jours, ces trois concepts sont souvent mélangés pour tirer parti du meilleur de chaque solution. De même, la façon de passer du code source au code objet fait toujours l'objet de recherche et d'amélioration.

Tout ça pourra vous paraître un peu compliqué et encore un peu obscur. Si vous vous lancez demain dans le codage, c'est mieux de connaître ces principes dans leurs grandes lignes. Mais si votre premier projet c'est d'écrire un compilateur, eh bien, bon courage !...

Sommaire - En savoir plus