Chapitre 1 · 4 h
Grammaires pour la compilation
Arbre syntaxique concret et abstrait ; ambiguïté, priorité et associativité des opérateurs ; élimination de la récursivité gauche ; factorisation gauche.
L'analyse syntaxique est le cœur du cours, et ce chapitre en prépare le terrain. La suite de tokens produite par le lexeur (chapitre 2) est plate ; un programme, lui, est une structure : expressions imbriquées, blocs, conditions. Reconstruire cette structure demande une grammaire — et pas n'importe laquelle. Une grammaire mathématiquement correcte peut être inutilisable par un analyseur ; ce chapitre montre comment la mettre en forme.
Vous connaissez déjà les grammaires hors contexte depuis la Théorie des langages. On les reprend ici avec l'œil du compilateur : ce qui compte n'est plus seulement le langage engendré, mais la forme des arbres et l'aptitude de la grammaire à être analysée.
Arbre concret, arbre abstrait
L'analyse syntaxique produit un arbre. Il en existe deux variantes, qu'il faut distinguer.
L'arbre syntaxique concret (ou arbre de dérivation, cf. Théorie des langages) reflète toutes
les règles appliquées, y compris les non-terminaux intermédiaaires et les détails de ponctuation. Pour
3 + 4 * 5 avec une grammaire à niveaux E → E + T, T → T * F, F → nombre, il contient des nœuds
E, T, F en cascade, ainsi que les parenthèses éventuelles.
L'arbre syntaxique abstrait (AST) ne garde que l'essentiel du sens : les opérateurs et leurs
opérandes. Le même 3 + 4 * 5 devient simplement :
(+) / \ 3 (*) / \ 4 5Plus de T, de F, ni de parenthèses : elles ont joué leur rôle (fixer la structure) et disparaissent.
C'est l'AST qui circule dans tout le reste du compilateur — analyse sémantique, génération de code. Le
concret sert à l'analyse, l'abstrait à la suite.
Ambiguïté, priorité, associativité
Le danger d'une grammaire, on l'a vu en Théorie des langages, est l'ambiguïté : un mot ayant
plusieurs arbres, donc plusieurs sens. Pour un compilateur, c'est rédhibitoire — 1 + 2 * 3 ne peut
pas valoir tantôt 7, tantôt 9.
On lève l'ambiguïté en encodant priorité et associativité dans la structure de la grammaire, par des niveaux :
E → E + T | T (+ : priorité faible, en haut)T → T * F | F (* : priorité forte, plus bas)F → ( E ) | nombreLa règle est mécanique : plus un opérateur est prioritaire, plus il est bas dans la grammaire —
donc plus profond dans l'arbre, donc évalué en premier. Un niveau par priorité. Quant à
l'associativité, elle se lit dans le sens de la récursivité : E → E + T (récursif à gauche)
donne une addition associative à gauche, ce qui est correct pour - et / (5 - 3 - 1 doit
valoir (5-3)-1).
Quiz · 1 question
Dans la grammaire E → E + T | T, T → T * F | F, F → (E) | nombre, pourquoi la multiplication est-elle prioritaire sur l'addition ?
- Parce que * apparaît alphabétiquement après + — ordre des symboles
- Parce que T (qui porte le *) est plus bas que E (qui porte le +) : plus profond dans l'arbre, donc évalué en premier — profondeur dans l'arbre
- Parce que la grammaire est ambiguë et Bison choisit * — choix de l'outil
Réponse : La priorité est encodée dans la STRUCTURE : E (addition) se dérive en T (multiplication), qui se dérive en F. Le * est donc toujours plus BAS dans la grammaire, donc plus profond dans l'arbre de « 1 + 2 * 3 » — le sous-arbre 2*3 se forme sous le +, et sera évalué en premier. Ce n'est ni une question d'ordre alphabétique, ni un arbitrage d'outil : la grammaire n'est pas ambiguë, elle impose un seul arbre, celui qui donne 7.
Éliminer la récursivité gauche
Voici le premier obstacle de méthode. Une grammaire récursive à gauche — un non-terminal dont
une règle commence par lui-même, comme E → E + T — est parfaite pour la lecture, mais fatale à
l'analyse descendante (chapitre 4) : la fonction chargée d'analyser E commencerait par s'appeler
elle-même sans consommer aucun token, en boucle infinie.
On l'élimine par une transformation mécanique, sans changer le langage engendré :
L'idée : au lieu d'empiler E à gauche, on écrit β (le cas de base) suivi d'une répétition de
α. C'est le passage d'une récursion gauche à une récursion droite — équivalente pour le langage, mais
analysable de haut en bas. Un effet de bord à connaître : la transformation inverse
l'associativité (elle associe à droite), qu'il faudra rétablir à la construction de l'AST pour les
opérateurs non commutatifs. L'exercice de ce chapitre déroule précisément cette transformation.
La factorisation gauche
Le second obstacle. Quand deux règles d'un même non-terminal commencent par le même préfixe, l'analyseur descendant ne peut pas choisir laquelle appliquer avec un seul token d'avance :
instr → if ( E ) instr (deux règles qui commencent par « if ( E ) instr ») | if ( E ) instr else instrOn factorise le préfixe commun dans une règle, et on repousse la partie qui diffère dans un non-terminal auxiliaire :
Après factorisation, l'analyseur lit d'abord le préfixe commun γ, puis décide entre β₁ et β₂ —
un choix qu'un seul token d'avance suffit désormais à trancher.
Récursivité gauche et facteur commun sont les deux préparations qu'une grammaire doit subir avant l'analyse descendante. Le chapitre 4 les suppose faites.
Quiz · 1 question
Pourquoi une grammaire récursive à gauche (E → E + T | T) est-elle inutilisable par un analyseur descendant récursif ?
- Parce qu'elle engendre un langage ambigu — ambiguïté
- Parce que la fonction analysant E s'appellerait elle-même sans consommer de token, en récursion infinie — récursion infinie
- Parce qu'elle ne peut pas décrire les priorités d'opérateurs — priorités
Réponse : L'analyse descendante associe une fonction à chaque non-terminal, qui applique une règle en lisant de gauche à droite. Avec E → E + T, la fonction analyserE() commencerait par... appeler analyserE(), sans avoir lu le moindre token : récursion infinie, la pile déborde. Le langage n'est ni ambigu ni inexpressif — c'est la FORME de la grammaire qui bloque la méthode descendante. On la corrige en éliminant la récursivité gauche.
À vous
L'exercice applique la transformation clé du chapitre : éliminer la récursivité gauche de
E → E + T | T, et vérifier que la grammaire obtenue engendre exactement le même langage. Vous
observerez l'effet sur l'associativité — le piège à connaître — et la seconde préparation utile, la
factorisation gauche.
C'est la mise en forme qui rend possible l'analyseur descendant du chapitre suivant, prochaine couche du compilateur du TP.
Exercice de code
Éliminez la récursivité gauche de la grammaire E → E + T | T par la transformation standard, et vérifiez que la grammaire obtenue engendre exactement le même langage. Notez au passage l'effet sur l'associativité, et la seconde préparation utile : la factorisation gauche.
Point de départ
// La grammaire des additions, telle qu'on l'écrit naturellement :
// E -> E + T | T
// Elle est RÉCURSIVE À GAUCHE : le membre droit de la 1re règle commence par
// E lui-même. Un analyseur descendant récursif appellerait analyserE() qui
// rappellerait analyserE() sans rien consommer -> boucle infinie.
//
// La transformation standard : A -> A alpha | beta devient
// A -> beta A'
// A' -> alpha A' | epsilon
// Ici A = E, alpha = "+ T", beta = "T".
// On teste l'équivalence en ENGENDRANT les mots (jusqu'à une longueur), avec
// un terminal 't' pour T et le symbole '+'.
function motsGauche(max) {
// E -> E + T | T (récursive à gauche) : E = t (+ t)*
const s = new Set();
let courant = "t";
while (courant.length <= max) { s.add(courant); courant += "+t"; }
return s;
}
// ── À VOUS : engendrer avec la grammaire TRANSFORMÉE ────────────────────────
// E -> T E'
// E' -> + T E' | epsilon
// (avec T = t). Écrire motsDroite(max) qui engendre le même ensemble.
function motsDroite(max) {
const s = new Set();
// à compléter : partir de "t", et tant que possible, ajouter "+t"
return s;
}
// ── Vérification ────────────────────────────────────────────────────────────
const g = [...motsGauche(9)].sort();
const d = [...motsDroite(9)].sort();
console.log("gauche :", g.join(" "));
console.log("droite :", d.join(" "));
console.log("mêmes mots ?", JSON.stringify(g) === JSON.stringify(d));
Solution
function motsDroite(max) {
// E -> T E' ; E' -> + T E' | epsilon engendre t, t+t, t+t+t, ...
// c'est-à-dire EXACTEMENT le même langage, mais en associant à DROITE
// pendant la dérivation. L'ensemble des mots est identique.
const s = new Set();
let courant = "t";
while (courant.length <= max) { s.add(courant); courant += "+t"; }
return s;
}
// gauche et droite engendrent { t, t+t, t+t+t, ... } : mêmes mots. ✓
// ── Ce que l'exercice enseigne ──────────────────────────────────────────────
//
// 1. La récursivité gauche est INCOMPATIBLE avec la descente récursive : la
// fonction analyserE() commencerait par s'appeler elle-même sans avoir
// consommé le moindre token -> récursion infinie. C'est un obstacle de
// MÉTHODE, pas de langage : le langage est parfaitement analysable, c'est
// l'écriture de la grammaire qui bloque.
//
// 2. La transformation A -> A alpha | beta => A -> beta A', A' -> alpha A' | eps
// engendre le MÊME langage. On ne change pas ce qui est reconnu, seulement
// la FORME des règles, pour la rendre analysable de haut en bas.
//
// 3. Attention : cette transformation change l'ASSOCIATIVITÉ implicite des
// arbres (elle associe à droite). Pour un opérateur comme la soustraction,
// il faudra rétablir l'associativité gauche à la construction de l'arbre
// (chapitre 4) — sinon « 5 - 3 - 1 » se calculerait 5 - (3 - 1) = 3 au lieu
// de (5 - 3) - 1 = 1.
//
// 4. L'autre préparation courante est la FACTORISATION GAUCHE : deux règles
// d'un même non-terminal qui commencent pareil (A -> a b | a c) empêchent
// de choisir avec un seul symbole d'avance ; on factorise en
// A -> a A', A' -> b | c. Récursivité gauche et facteur commun sont les
// deux obstacles que le chapitre 4 exige d'avoir levés.
Ce que la suite en fait
La grammaire est prête. Les deux chapitres suivants la mettent au travail selon deux stratégies opposées et complémentaires.
Le chapitre 4 construit l'arbre par le haut (analyse descendante) : on part de l'axiome et on prédit les règles à appliquer — la méthode la plus intuitive, celle qu'on écrit à la main, et qui exige justement une grammaire débarrassée de récursivité gauche et de facteurs communs. Le chapitre 5 construira l'arbre par le bas (analyse ascendante), plus puissante mais moins intuitive.
À retenir
Flashcards · 4 cartes
- Quelle est la différence entre arbre syntaxique concret et abstrait (AST) ?
- L'arbre CONCRET (de dérivation) reflète toutes les règles appliquées, non-terminaux intermédiaires (E, T, F) et ponctuation compris. L'AST ne garde que l'essentiel du sens : opérateurs et opérandes (un nœud + avec ses deux fils). Le concret sert à l'analyse ; l'AST, plus léger, circule dans tout le reste du compilateur (sémantique, génération de code).
- Comment encode-t-on priorité et associativité des opérateurs dans une grammaire ?
- Par des NIVEAUX : un non-terminal par niveau de priorité. Plus un opérateur est prioritaire, plus il est BAS dans la grammaire (E → …+… au-dessus de T → …*…), donc plus profond dans l'arbre, donc évalué en premier. L'associativité se lit dans la récursivité : récursif à gauche (E → E + T) = associatif à gauche, correct pour - et /.
- Pourquoi et comment élimine-t-on la récursivité gauche ?
- Parce qu'elle rend l'analyse descendante impossible (récursion infinie : la fonction de E s'appelle sans consommer de token). Transformation : A → A α | β devient A → β A', A' → α A' | ε (récursion droite équivalente). Attention : cela inverse l'associativité (associe à droite), à rétablir à la construction de l'AST pour - et /.
- Qu'est-ce que la factorisation gauche, et quel problème résout-elle ?
- Quand deux règles d'un non-terminal commencent par le même préfixe (A → γβ₁ | γβ₂), l'analyseur descendant ne peut pas choisir avec un seul token d'avance. On factorise : A → γ A', A' → β₁ | β₂ — on lit d'abord le préfixe commun, puis on décide. Avec l'élimination de la récursivité gauche, c'est l'une des deux préparations exigées avant l'analyse descendante.