cursus.

Cours 1 · Vue d'ensembleLeçon 1 sur 1

Anatomie d'un compilateur

6 h de lecture8 sections Version PDF

À la fin de cette leçon, vous saurez

Compilation contre interprétation ; les phases d'analyse et de synthèse ; table des symboles et gestion des erreurs comme services transversaux ; front-end, back-end et l'intérêt de les séparer.

Vous savez déjà, depuis la Théorie des langages, reconnaître un langage avec un automate, l'engendrer avec une grammaire, et découper un texte en unités lexicales. Ce cours répond à la question qui suit naturellement : une fois le texte reconnu, comment le traduit-on en un programme qui s'exécute ?

Un compilateur est l'un des plus beaux objets du génie logiciel — une machine qui lit un langage et en écrit un autre, en préservant le sens. Ce premier chapitre en donne la carte : les phases, leur ordre, ce que chacune reçoit et produit. Chaque bloc du cours en remplira ensuite une case, et le TP du semestre les assemblera toutes dans un compilateur unique pour un mini-langage impératif.

Compiler ou interpréter

Il y a deux façons d'exécuter un programme écrit dans un langage de haut niveau.

Un compilateur traduit le programme entier vers un autre langage (souvent le code machine) une fois pour toutes ; le résultat s'exécute ensuite seul, sans le compilateur. Un interpréteur exécute le programme directement, instruction par instruction, sans produire de traduction autonome.

CompilationInterprétation
Moment de l'analyseune fois, avant exécutionà chaque exécution
Vitesse d'exécutionrapide (code natif)plus lente (surcoût permanent)
Souplessefigée à la compilationdynamique, exécution directe
ExemplesC, Rust, GoPython, Ruby (historiquement)

La frontière est floue en pratique : Java compile vers un bytecode, ensuite interprété (ou recompilé à la volée) par une machine virtuelle. C'est d'ailleurs la cible que visera le TP — produire du code pour une petite machine virtuelle est plus simple que viser un vrai processeur, et n'enlève rien à la démarche. Ce cours porte sur la compilation, mais l'essentiel de ses phases (tout le front-end, plus bas) est commun aux deux mondes : un interpréteur analyse le programme exactement comme un compilateur, il diffère seulement dans ce qu'il fait de l'arbre à la fin.

Analyse et synthèse

Un compilateur se divise en deux grands mouvements.

L'analyse (ou front-end) décompose le programme source pour en extraire la structure et le sens : elle lit du texte et en construit une représentation interne. Si le programme est incorrect, c'est l'analyse qui le refuse, avec des messages d'erreur.

La synthèse (ou back-end) construit le programme cible à partir de cette représentation : elle produit du code, et l'optimise.

Cette césure n'est pas cosmétique — c'est l'idée d'ingénierie centrale du chapitre, on y revient plus bas. Retenez déjà la forme générale : on remonte du texte vers une représentation abstraite, puis on redescend de cette abstraction vers du code concret.

Le schéma complet

Voici la chaîne des phases, dans l'ordre. C'est le plan du cours ; gardez-le sous les yeux.

    programme source (texte)   ┌────────▼─────────┐   │ analyse lexicale │  texte → suite de lexèmes            (bloc II)   └────────┬─────────┘   ┌────────▼──────────┐   │ analyse syntaxique│  lexèmes → arbre syntaxique         (bloc III)   └────────┬──────────┘   ┌────────▼──────────┐   │ analyse sémantique│  arbre → arbre vérifié et typé      (bloc IV)   └────────┬──────────┘   ┌────────▼──────────────┐   │ code intermédiaire     │  arbre → représentation linéaire (bloc V)   └────────┬──────────────┘   ┌────────▼──────────┐   │ optimisation       │  code → code équivalent, meilleur  (bloc VI)   └────────┬──────────┘   ┌────────▼──────────┐   │ génération de code │  → code cible                       (bloc VI)   └────────┬──────────┘    programme cible

Chaque phase consomme la sortie de la précédente : l'analyse syntaxique ne voit jamais le texte, seulement les lexèmes ; l'analyse sémantique ne voit que l'arbre. C'est ce qui rend l'ensemble modulaire — et ce qui explique quelle erreur est détectée où. Une addition entre un entier et une chaîne est bien formée lexicalement et syntaxiquement ; seule l'analyse sémantique, qui raisonne sur le sens, peut la refuser.

Deux services transversaux

Deux fonctions ne sont pas des phases : elles accompagnent toutes les phases, du début à la fin.

La table des symboles enregistre tout ce qu'on sait des identificateurs du programme — nom, type, portée, emplacement mémoire. Elle est remplie pendant l'analyse (déclarations) et consultée jusqu'à la génération de code. C'est la mémoire partagée du compilateur, et le chapitre 6 lui est consacré.

La gestion des erreurs doit détecter les fautes, les rattacher à la bonne phase et au bon endroit du source, et si possible poursuivre l'analyse pour signaler plusieurs erreurs d'un coup plutôt que de s'arrêter à la première. La qualité des messages d'erreur est, pour l'utilisateur, la moitié de la valeur d'un compilateur.

Quiz · vérifiez votre compréhension Sans réponse

Le programme « y = 3 + 'bonjour' » (additionner un entier et une chaîne) est refusé par le compilateur. Quelle phase le détecte, et pourquoi pas plus tôt ?

Front-end, back-end : pourquoi séparer

Reprenons la césure analyse/synthèse, car c'est l'idée qui structure toute l'industrie du compilateur.

Le front-end dépend du langage source : il sait lire du C, ou du Java, ou votre mini-langage. Le back-end dépend de la machine cible : il sait produire du code x86, ou ARM, ou du bytecode. Entre les deux, une représentation intermédiaire (bloc V) sert de pivot : le front-end y aboutit, le back-end en part.

L'intérêt est combinatoire. Avec mm langages et nn machines, une approche monolithique demanderait m×nm \times n compilateurs distincts. En passant par un pivot commun, il suffit de mm front-ends et nn back-ends, soit m+nm + n composants — chaque nouveau langage réutilise tous les back-ends existants, et chaque nouvelle machine, tous les front-ends. C'est exactement l'architecture de projets comme LLVM ou GCC : un front-end par langage, un back-end par processeur, une représentation intermédiaire au centre. La séparation n'est pas une élégance théorique, c'est ce qui rend l'écosystème viable.

Quiz · vérifiez votre compréhension Sans réponse

Une entreprise veut compiler 4 langages vers 5 architectures de processeurs. Combien de composants faut-il avec une représentation intermédiaire commune, contre une approche monolithique ?

À vous

L'exercice met à l'épreuve la compréhension du pipeline, qui est tout l'objet de ce chapitre : ranger chaque erreur dans la phase qui la détecte. Vous verrez pourquoi l'analyseur lexical ne peut pas repérer une erreur de type, pourquoi une parenthèse manquante relève du syntaxique, et pourquoi les phases forment un filtre en cascade.

Savoir une erreur est attrapée, c'est déjà savoir dans quelle partie du compilateur — et du TP — elle se corrige.

Exercice · JavaScript · à vous de jouer

Rangez chaque erreur dans la phase du compilateur qui la détecte : lexicale, syntaxique ou sémantique. Comprenez pourquoi l'analyseur lexical ne peut pas voir une erreur de type, et pourquoi les phases forment un filtre en cascade.

En attente
// Un compilateur est une CHAÎNE de phases. Chacune reçoit la sortie de la
// précédente et ne détecte que les erreurs de son niveau :
//
//   texte --[lexical]--> lexèmes --[syntaxique]--> arbre --[sémantique]--> arbre typé
//
//   - lexicale   : un caractère ou un lexème impossible (ex. « @ », « 12abc »)
//   - syntaxique : des lexèmes valides mal AGENCÉS (ex. « x = ; », parenthèse)
//   - sémantique : une phrase bien formée mais qui n'a pas de SENS
//                  (ex. additionner un entier et une chaîne, variable inconnue)
//
// À VOUS : complétez classer() pour ranger chaque erreur dans sa phase.

const CAS = [
  { src: "x = 12abc + 1",        attendu: "lexicale"   },  // 12abc : nombre mal formé
  { src: "x = (3 + 4",           attendu: "syntaxique" },  // parenthèse jamais fermée
  { src: "y = 3 + \"bonjour\"", attendu: "semantique" },  // entier + chaîne
  { src: "z = @val",             attendu: "lexicale"   },  // @ : caractère interdit
  { src: "if x > 0 then",        attendu: "syntaxique" },  // then sans bloc, forme cassée
  { src: "w = variableInconnue", attendu: "semantique" },  // identifiant non déclaré
];

function classer(src) {
  // Indices à repérer (dans l'ordre lexical -> syntaxique -> sémantique) :
  //  - un caractère hors alphabet, ou un chiffre collé à des lettres -> lexicale
  //  - parenthèse non fermée, ou une construction incomplète -> syntaxique
  //  - type incompatible, ou identifiant inconnu -> sémantique
  return "?"; // à compléter
}

for (const c of CAS) {
  const r = classer(c.src);
  console.log((r === c.attendu ? "  ok " : " ✗   ") + c.attendu.padEnd(11) + " | " + c.src);
}

Console de sortie
Le résultat s'affiche dans la console

Ce que la suite en fait

Ce chapitre a posé la carte ; les dix suivants la parcourent, dans l'ordre du flot.

Le bloc II descend dans la première phase, l'analyse lexicale — là où la Théorie des langages rejoint directement la compilation, puisqu'un analyseur lexical n'est rien d'autre qu'un automate fini. Puis le bloc III, cœur du cours, construit l'arbre syntaxique par analyse descendante et ascendante ; le bloc IV lui donne un sens ; les blocs V et VI le transforment enfin en code.

Gardez en tête le fil du TP : à partir du prochain chapitre, vous écrivez un vrai compilateur, une couche par bloc, sur un mini-langage unique. Le schéma de ce chapitre en est le plan de montage.

À retenir

Flashcards · 1 / 4Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 8 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.