Skip to content

2 2 2 2 Compilateur&interpréteur

Compilation, interprétation et runtime

Un programme constitue un code source, un fichier contenant un texte, c'est-à-dire dont les éléments sont des caractères. L'extension (ou suffixe) du nom de fichier (comme .py, .c, .cpp, .jav, ...) est traditionnellement associée à un langage particulier ou un élément de ce langage.

Un langage peut de base être soit compilé (comme C, C++, Fortran,Cobol, Java, OCaml) soit interprété (comme Python, Basic, Ruby, PHP, Javascript).

Notons que le résultat de la traduction d'un programme en Java est, en général, du « bytecode » destiné à être interprété par une Java Virtual Machine (JVM).

Langage compilé

Dans ce cas, l'exécution d'un programme passe par trois phases séparées : la compilation, le chargement (avec l'édition des liens) et l'exécution proprement dite.

Un compilateur est un programme, un logiciel qui transforme un fichier contenant un code source, écrit dans un langage de programmation particulier, en un autre fichier contenant un code binaire (en langage machine) ou, plus généralement, un code objet, dans le format correspondant à la machine cible (architecture matérielle et logicielle).

Principe du compilateur

Notons qu'un compilateur a trois langages de programmation comme paramètres principaux :

  • le langage source des programmes qu'il compile
  • le langage cible dans lequel il compile les programmes, et
  • le langage dans lequel il est lui-même écrit.

Par exemple gfortran (https://gcc.gnu.org/fortran/) est un compilateur qui traduit les codes FORTRAN en (par exemple) code machine Intel i386, et est écrit en langage C.

Il est plaisant de constater que si « compilation », dans ce sens particulier, et « compilateur » semblent provenir d'une francisation de termes anglais, c'est en fait l'inverse qui s'est produit : le verbe anglais « to compile » et ses dérivés sont issus du verbe « compiler » en vieux français (début du XIVe siècle) signifiant « recueillir, rassembler ». Il existe de nombreux termes scientifiques ou techniques qui ont une histoire similaire, car l'anglais savant est d'origine latine et francophone. À titre d'exemple, « randomiser » (donner un caractère aléatoire) semble provenir de l'anglais « randomize », de « random », mais l'origine réelle de ces termes est l'ancien mot français « randon » (promenade aléatoire, impétueuse) ou le verbe « randir », radical que l'on retrouve encore dans « randonnée », par exemple.

Phases de la compilation

Pour traduire un programme, un compilateur doit analyser la source pour y identifier les éléments du langage et leur signification. La connaissance de ce processus d'analyse est la clé de la compréhension des contraintes d'écriture d'un langage de programmation.

Les phases d'analyse incluent :

  • l'analyse lexicale (également appelée scanning en anglais), qui découpe la séquence de caractères du code source en blocs atomiques appelés jetons (tokens en anglais). Chaque jeton appartient à une unité atomique unique du langage appelée unité lexicale ou lexème (par exemple un mot-clé, un identifiant ou un symbole). Le logiciel qui effectue une analyse lexicale est appelé un analyseur lexical ou un scanner. Deux outils classiques permettent de construire plus aisément des scanners : lex et flex.

  • l'analyse syntaxique (également appelée parsing en anglais) implique l'analyse de la séquence de jetons pour identifier la structure syntaxique du programme. Cette phase s'appuie généralement sur la construction d'un arbre d'analyse (appelé communément arbre syntaxique) ; on remplace la séquence linéaire des jetons par une structure en arbre construite selon la grammaire formelle qui définit la syntaxe du langage. Par exemple, une condition est toujours suivie d'un test logique (égalité, comparaison, ...). L'arbre d'analyse est souvent modifié et amélioré au fur et à mesure de la compilation. YACC et GNU Bison sont les outils les plus utilisés pour construire des analyseurs syntaxiques.

  • l'analyse sémantique est la phase durant laquelle le compilateur ajoute des informations sémantiques (donner un « sens ») à l'arbre syntaxique. Cette phase fait un certain nombre de contrôles : vérifie le type des éléments (variables, fonctions, ...) utilisés dans le programme, leur visibilité, vérifie que toutes les variables locales utilisées sont initialisées, .... L'analyse sémantique nécessite habituellement un arbre syntaxique complet, ce qui signifie que cette phase fait suite à la phase d'analyse syntaxique, et précède logiquement la phase de génération de code, mais il est possible de replier ces phases en une seule passe qui effectiue les trois phases en "parallèle".

les trois phases d'analyse construisent et complètent la table des symboles qui centralise les informations attachées aux identificateurs du programme. Dans une table des symboles, on retrouve des informations comme : le type, l'emplacement mémoire, la portée, la visibilité, etc.

C'est au cours de ces phases que le compilateur détecte les erreurs éventuelles (par rapport à la définition du langage) et produit les messages d'erreur permettant à l'auteur du programme de poursuivre sa mise au point. La compréhension de ce processus est de nature à mieux comprendre le sens de ces messages : selon la phase pendant laquelle ils sont générés, ils décrivent des erreurs de types différents.

Un prétraitement peut également être effectué avant ou de façon entremêlée aux phases d'analyse. Un tel prétraitement est nécessaire pour certaines langues comme C où il prend en charge la substitution de macro et de la compilation conditionnelle, permettant par exemple de générer des codes différents en fonction de paramètres.

Phases de la compilation

Vient ensuite la génération du code proprement dite, avec ses phases d'optimisation : allocation des registres, suppression de « code mort » (qui ne pourra jamais être exécuté), sortie de boucle d'opérations indépendantes de l'itération, sortie d'alternative des codes communs, permutation d'instructions, etc.

La généraion de code inclut les trois phases :

  • la transformation du code source en code intermédiaire (appelée image) ;

  • optimisation du code intermédiaire : c'est-à-dire rendre le programme « meilleur » selon son usage ;

  • la génération de code final avec l'allocation de registres et la traduction du code intermédiaire en code objet, avec éventuellement l'insertion de données de débogage et d'analyse de l'exécution.

L'analyse lexicale, syntaxique et sémantique, le passage par un langage intermédiaire et l'optimisation forment la partie frontale. La génération de code final constitue la partie finale.

Ceci permet de minimiser le travail quand on doit construire plusieurs compilateurs pour des langages différents et pour des plateformes (ou langages cibles différents) (voir figure ci-dessous)

Exemples de parties frontales et finales de compilateurs

Après la compilation suit une phase d'enrichissement avec des modules issus de bibliothèques (libraries) qui font partie de l'environnement d'exécution (runtime) pour la machine cible. L'intégration en un code complet de tous ces éléments est ce qu'on appelle l'édition des liens (link). Cette édition de liens permet non seulement de relier un programme avec certains modules de la bibliothèque, mais également de combiner plusieurs codes objets correspondant à des morceaux de programme, éventuellement écrits dans des langages différents, qui ont fait l'objet d'une compilation séparée.

Bribes d'exemple illustrant les phases de la compilation

Définition formelle d'un langage de programmation : syntaxe et sémantique

Les détails des différentes phases de la compilation et les algorithmes complexes et structures de données conçus à cette fin sont notamment le sujet d'étude de cours de « Théorie des langages et de la compilation ». Outre le fonctionnement d'un compilateur, on y trouve l'étude des différentes classes de langages formels utilisés pour définir un langage de programmation.

En particulier la base de la théorie des langages repose sur l'étude de la hiérarchie de Chomsky décrite par Noam Chomsky en 1956.

Ainsi les unités lexicales et les lexèmes possibles sont généralement formellement définis grâce à des langages réguliers, la syntaxe d'un langage de programmation est définie grâce aux grammaires algébriques, ....

Notons que la sémantique d'un langage de programmation est beaucoup plus difficile à formaliser. Elle utilise par exemple la notion de grammaire attribuée, plus difficile à appréhender que les grammaires "simples".

Nous n'analysons pas ici les différents éléments d'un langage et en particulier du langage Python dont la connaissance de base est un prérequis à cette partie de cours, et référons le lecteur désireux d'appronfondir la matière au manuel de référence du langage Python où la grammaire complète de Python est donnée.

Notons que dans la plupart des langages de programmation, la notation utilisée pour définir la syntaxe du langage est principalement l' Extended Backus-Naur Form (EBNF). Il est important de comprendre cette notation pour pouvoir lire ces définitions de langage.

Langage interprété

Dans ce cas, l'interpréteur suit un cycle de lecture, d'analyse et d'exécution immédiate d'une instruction avant de passer à l'instruction suivante.