Compilateur

Restez toujours informé : suivez-nous sur Google (☆)

Introduction

Schéma de compilation multi-source multi-cible

Un compilateur est un programme informatique qui traduit un langage, le langage source, en un autre, appelé le langage cible (ou langage objet), en préservant la signification du texte source. Ce schéma général décrit un grand nombre de programmes différents ; et ce que l'on entend par « signification du texte source » dépend du rôle du compilateur. Lorsque l'on parle de compilateur, on suppose aussi en général que le langage source est, pour l'application envisagée, de plus haut niveau que le langage cible, c'est-à-dire qu'il présente un niveau d'abstraction supérieur.

En pratique, un compilateur sert le plus souvent à traduire un code source écrit dans un langage de programmation en un autre langage, habituellement un langage d'assemblage ou un langage machine. Le programme en langage machine produit par un compilateur est appelé code objet.

Le premier compilateur, A-0 System, a été écrit en 1951 par Grace Hopper.

Structure et phases d'un compilateur

La tâche principale d'un compilateur est de produire du code objet correct. La plupart des compilateurs permettent d'optimiser le code (le code objet optimisé s'exécutera plus rapidement, ou aura une occupation mémoire moindre).

Un compilateur fonctionne par analyse-synthèse, c'est-à-dire qu'au lieu de remplacer chaque construction du langage source par une suite équivalente de constructions du langage cible, il commence par analyser le texte source pour en construire une représentation intermédiaire qu'il traduit à son tour en langage cible.

Il est donc naturel de séparer — au moins conceptuellement, mais aussi en pratique — le compilateur en une partie avant (ou frontale), parfois appelée « souche », qui lit le texte source et produit la représentation intermédiaire, et une partie arrière (ou finale), qui parcourt cette représentation pour produire le texte cible. Dans un compilateur idéal, la partie avant est indépendante du langage cible, tandis que la partie arrière est indépendante du langage source. Certains compilateurs effectuent de plus sur la forme intermédiaire des traitements substantiels, que l'on peut regrouper en une partie centrale, indépendante à la fois du langage source et de la machine cible. On peut ainsi écrire des compilateurs pour toute une gamme de langages et d'architectures en partageant la partie centrale, à laquelle on attache une partie avant par langage et une partie arrière par architecture.

Les étapes de la compilation incluent :

Chaque jeton est une unité atomique unique de la langue (unités lexicales ou lexèmes), par exemple un mot-clé, identifiant ou nom du symbole. La syntaxe de jeton est généralement un langage régulier, donc un automate à états finis construits à partir d'une expression régulière peut être utilisé pour le reconnaître. Cette phase est aussi appelée lexing ou à balayage, et le logiciel qui effectue une analyse lexicale est appelé un analyseur lexical ou un scanner (lex, flex par exemples).

  • le prétraitement, phase nécessaire pour certaines langues, par exemple, C, qui prend en charge la substitution de macro et de la compilation conditionnelle.

Généralement, la phase de prétraitement se produit avant que l'analyse syntaxique ou sémantique, par exemple dans le cas de C, le préprocesseur manipule les symboles lexicaux plutôt que des formes syntaxiques.

  • l'analyse syntaxique implique l'analyse de la séquence jeton pour identifier la structure syntaxique du programme.

Cette phase s'appuie généralement sur un arbre d'analyse, qui remplace la séquence linéaire de jetons avec une structure en arbre construit selon les règles d'une grammaire formelle qui définit la syntaxe du langage. L'arbre d'analyse est souvent analysé, augmenté et transformé par des phases plus tard dans le compilateur. Yacc et Bison sont les analyseurs syntaxiques les plus utilisés.

Cette phase effectue des vérifications sémantiques comme la vérification de type (vérification des erreurs de type), ou objet de liaison (associant variables et des références de fonction avec leurs définitions), ou tâche définie (nécessitant toutes les variables locales doivent être initialisées avant utilisation), le rejet des programmes incorrects ou l'émission d'avertissements. L'analyse sémantique nécessite habituellement un arbre d'analyse complet, ce qui signifie que cette phase est la conséquence logique de la phase d'analyse, et précède logiquement la phase de génération de code, mais il est souvent possible de replier les phases multiples dans une passe sur le code dans une mise en œuvre de compilateur.

  • la transformation du code source en code intermédiaire ;
  • l'application de techniques d'optimisation sur le code intermédiaire : c'est à dire rendre le programme plus rapide, tout en gardant des délais de compilation raisonnables.

Le but de cette phase est de générer un code réduit, permises par des passes de compilation qui se greffent par dessus un compilateur existant. C’est un domaine de recherche continuel !

  • la génération de code 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 ;
  • et enfin l'édition des liens.

Ces différentes étapes expliquent que les compilateurs fassent toujours l'objet de recherches, particulièrement dans le domaine de l'optimisation du code produit.

La plupart (mais pas tous) des compilateurs traduisent un fichier source d'un programme écrit dans un langage de programmation en un fichier objet (ou un exécutable, ou un fichier en assembleur, ou même en un autre langage).

Une implémentation (réalisation concrète) d'un langage de programmation peut être interprétée ou compilée. C'est cette réalisation qui est un compilateur ou un interpréteur, et un langage de programmation (spécification plus ou moins théorique et formalisée de sa syntaxe et de sa sémantique) peut avoir une implémentation compilée, et une autre interprétée.

Les qualités d'un compilateur

La qualité la plus importante est bien sûr d'obtenir le « zéro bug », c'est donc la confiance. Ensuite, il y a le fait que le code généré doit être rapide : c'est l'optimalité. Le compilateur lui-même doit être rapide : l'efficacité. Le compilateur doit être compatible sur plusieurs plateformes : la portabilité. Les erreurs signalées doivent être utilisées : la productivité.

Compilateur simple passe et multi passe

Compilateur multi-passes typique

La classification des compilateurs par nombre de passes a pour origine le manque de ressources matérielles des ordinateurs. La compilation est un processus couteux et les premiers ordinateurs n'avaient pas assez de mémoire pour contenir un programme qui devait faire ce travail. Les compilateurs ont donc été divisés en sous programmes qui font chacun une passe sur la source (d’une forme ou d’une autre) pour accomplir les différentes phases d’analyse lexicale, d'analyse syntaxique et d'analyse sémantique.

La capacité de combiner le tout en un seul passage a été considérée comme un avantage car elle simplifie la tâche d'écrire d’un compilateur et il compile généralement plus rapidement qu’un compilateur multi passe. Ainsi, alimentée en partie par des ressources limitées des premiers systèmes, de nombreux langages ont été spécifiquement conçus afin qu'ils puissent être compilés en un seul passage (par exemple, le langage Pascal).

Dans certains cas, la conception d'une fonctionnalité de langage a besoin d'un compilateur pour effectuer plus d'une passe sur la source. Par exemple, considérons une déclaration figurant à la ligne 20 de la source qui affecte la traduction d'une déclaration figurant à la ligne 10. Dans ce cas, la première passe doit recueillir des renseignements sur les déclarations figurant après les déclarations qu'ils affectent, avec la traduction proprement dite qui s’effectue lors d'un passage ultérieur.

L'inconvénient de la compilation en un seul passage, c'est qu'il n'est pas possible d'exécuter la plupart des optimisations sophistiquées nécessaires pour générer du code de haute qualité. Il peut être difficile de dénombrer exactement le nombre de passes qu’un compilateur optimisant effectue.

Le fractionnement d'un compilateur en petits programmes est une technique utilisée par les chercheurs intéressés à produire des compilateurs performants. Prouver la justesse d'une série de petits programmes nécessite souvent moins d'effort que de prouver la justesse d'un plus grand programme unique équivalent.

Compilateurs particuliers

Compilateur croisé

Un compilateur croisé (en anglais Cross Compiler) est un programme capable de traduire un code source en code objet ayant un environnement d'exécution (architecture matérielle, système d'exploitation) différent de celui où la compilation est effectuée. Ces compilateurs sont principalement utilisés en informatique industrielle.

Byte code ou code octet

Certains compilateurs traduisent un langage source en langage machine virtuel, c'est-à-dire en un code (généralement une suite d'octets) exécuté par une machine virtuelle : un programme émulant les principales fonctionnalités d'un ordinateur. Le portage d'un programme ne requiert ainsi que le portage de la machine virtuelle. C'est le cas du compilateur Java, qui traduit du code Java en bytecode Java (code objet). Une machine virtuelle DotNet peut exécuter du bytecode MSIL produit par les langages de Microsoft C#, Visual Basic ou autres.

Autres compilateurs

Si la plupart des compilateurs traduisent un code d'un langage de programmation vers un autre, ce n'est pas le cas de tous les compilateurs. Par exemple, le logiciel LaTeX compile un code écrit dans le langage de formatage de texte LaTeX, pour le convertir en un autre langage, par exemple DVI, HTML, PostScript...

Certains compilateurs traduisent, de façon incrémentale ou interactive, le programme source (tapé par l'utilisateur) en du code machine. Par exemple, certaines implémentations de Common Lisp (comme SBCL) traduisent un bout de programme en du code machine (en mémoire).

Les compilateurs Just In Time (juste à temps) traduisent une représentation intermédiaire (souvent du code octet) en du code machine, de manière progressive.

Le problème de l'amorçage (bootstrap)

Les premiers compilateurs étaient écrits directement en langage assembleur, un langage symbolique élémentaire correspondant aux instructions du processeur cible et quelques structures de contrôle légèrement plus évoluées. Ce langage symbolique doit être assemblé (et non compilé) et lié pour obtenir une version exécutable. En raison de sa simplicité, un programme simple suffit à le convertir en instructions machines.

Les compilateurs actuels sont généralement écrits dans le langage qu'ils doivent compiler ; par exemple un compilateur C est écrit en C, SmallTalk en SmallTalk, Lisp en Lisp, etc. Dans la réalisation d'un compilateur, une étape décisive est franchie lorsque le compilateur pour le langage X est suffisamment complet pour se compiler lui-même : il ne dépend alors plus d'un autre langage (fut-ce de l'assembleur) pour être produit.

Les bugs des compilateurs sont parfois très complexes à détecter. Si un compilateur de langage C comporte un bug, les programmeurs en langage C auront naturellement tendance à mettre en cause leur propre code source, non pas le compilateur.

Pire, si ce compilateur buggé (version V1) compile un compilateur (version V2) non buggé, l'exécutable compilé (par V1) du compilateur V2 sera buggé. Pourtant son code source est bon. Le bootstrap oblige donc les programmeurs de compilateurs à contourner les bugs des compilateurs existants.