Analyse lexicale et syntaxiqueDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Théorie des langages · C4 Applications · Chapitre 1 · 6 h

Analyse lexicale et syntaxique

Du langage régulier à l'analyseur lexical ; analyse descendante LL(1) et tables d'analyse ; les outils Lex/Flex et Yacc/Bison ; le lien avec la compilation.

Toute la théorie du cours converge ici, dans son application reine : le compilateur. Quand vous écrivez prix * 12 + (tva * 20), deux étages de la hiérarchie de Chomsky se relaient pour donner un sens à cette suite de caractères. D'abord les langages réguliers — les automates finis des chapitres 3 à 6 — découpent le texte en unités. Ensuite les langages algébriques — les grammaires du chapitre 7, reconnues par les automates à pile du chapitre 8 — en reconstruisent la structure.

Ce chapitre montre cette chaîne à l'œuvre, et referme la boucle ouverte à la toute première ligne du cours : « une suite de caractères appartient-elle à un langage décrit à l'avance ? ». La réponse, pour un langage de programmation, se donne en deux temps.

L'analyse lexicale : le régulier au travail

L'analyse lexicale est la première phase. Son rôle : transformer le flot de caractères en une suite d'unités lexicales (ou tokens) — les mots du langage. Sur prix * 12, l'analyseur lexical (le lexer) produit :

IDENT(prix)   OP(*)   NOMBRE(12)

Chaque type d'unité est décrit par une expression régulière : un nombre est [0-9]+, un identifiant [a-z][a-z0-9]*, un opérateur l'un de + - * /. C'est exactement le monde du bloc II, et ce n'est pas un hasard : reconnaître ces motifs ne demande aucune mémoire non bornée. Un automate fini suffit.

Deux principes régissent le découpage :

La conséquence pratique est directe : puisqu'un lexer est un automate fini, on l'engendre à partir d'expressions régulières plutôt que de l'écrire à la main. C'est tout l'objet des outils vus plus bas, et c'est le lien concret entre le théorème de Kleene (chapitre 5) et un compilateur réel.

Quiz · 1 question

Pourquoi l'analyse lexicale peut-elle se faire avec un simple automate fini, alors que l'analyse syntaxique demande davantage ?

  • Parce que le lexer traite moins de caractères que l'analyseur syntaxiquevolume
  • Parce que reconnaître des unités (nombres, identifiants, opérateurs) ne demande aucune mémoire non bornée, alors que vérifier l'imbrication (parenthèses, blocs) exige de compter — donc une pilemémoire bornée vs comptage
  • Parce que l'analyse lexicale est facultative dans un compilateurfacultatif

Réponse : Découper le texte en unités relève de motifs réguliers : « une suite de chiffres », « une suite de lettres » se reconnaissent avec une mémoire bornée, donc un automate fini (chapitres 3-6). Vérifier la STRUCTURE — parenthèses équilibrées, blocs bien imbriqués — exige de compter les ouvertures et fermetures, ce qu'un automate fini ne sait pas faire (chapitre 6). Il faut une pile, donc l'analyse syntaxique (chapitres 7-8). Ce n'est pas une question de volume ni d'option : c'est la frontière régulier / algébrique.

L'analyse syntaxique : l'algébrique au travail

L'analyse syntaxique (le parsing) est la seconde phase. Elle prend la suite d'unités produite par le lexer et vérifie qu'elle respecte la grammaire du langage — puis en construit l'arbre de dérivation (chapitre 7), celui qui porte le sens.

C'est ici qu'intervient tout le bloc III. La grammaire d'un langage de programmation est une grammaire hors contexte ; la reconnaître demande une pile — parce qu'il faut vérifier l'imbrication des parenthèses, des blocs, des appels — donc un automate à pile (chapitre 8). Vérifier que les parenthèses de prix * (tva * 20) sont équilibrées, c'est très exactement le mécanisme « empiler/dépiler » de l'exercice du chapitre 8.

Et c'est ici que la désambiguïsation du chapitre 7 paie : la grammaire des expressions est écrite pour que 1+2*3 n'ait qu'un seul arbre, celui qui respecte la priorité du *. L'analyseur syntaxique en tire donc une structure, sans ambiguïté, et c'est cette structure que les phases suivantes du compilateur évalueront ou traduiront.

L'analyse descendante LL(1)

Parmi les méthodes d'analyse syntaxique, la plus simple à comprendre — et à programmer à la main — est l'analyse descendante LL(1). Son principe : construire l'arbre du haut vers le bas, en partant de l'axiome, et en ne regardant qu'une seule unité à l'avance.

C'est le sens du sigle : Lecture de gauche à droite, dérivation gauche (Leftmost), et 1 symbole de prévision. À chaque étape, l'analyseur doit décider quelle règle appliquer pour la variable courante — et il doit pouvoir trancher en ne voyant que la prochaine unité.

Cette décision se lit dans une table d'analyse : un tableau à double entrée (variable × unité suivante) qui indique la règle à appliquer. La construire, c'est calculer pour chaque variable ce par quoi ses dérivations peuvent commencer (les ensembles Premier) et, pour les règles pouvant s'effacer, ce qui peut suivre (les ensembles Suivant).

Une grammaire est LL(1) quand cette table n'a jamais deux règles dans une même case — sinon, l'analyseur ne saurait pas choisir avec un seul symbole d'avance. Toutes les grammaires ne sont pas LL(1) : certaines demandent de regarder plus loin, ou une méthode ascendante (LR, qui construit l'arbre du bas vers le haut et sous-tend l'outil Yacc/Bison). En L2, retenez le principe LL(1) et la notion de table de décision — les détails des ensembles Premier/Suivant relèvent du cours de compilation.

Quiz · 1 question

Dans « analyse LL(1) », que signifie le « 1 », et quelle contrainte impose-t-il à la grammaire ?

  • Une seule règle par variable dans toute la grammaireune règle par variable
  • Un seul symbole de prévision : l'analyseur doit pouvoir choisir la règle à appliquer en ne regardant que la prochaine unité — d'où une table sans conflitun symbole d'avance
  • Un seul passage sur le texte sourceun passage

Réponse : Le « 1 » de LL(1) désigne le nombre de symboles de PRÉVISION : l'analyseur décide quelle règle appliquer en ne consultant que la prochaine unité lexicale. Cela impose que la table d'analyse (variable × unité suivante) n'ait jamais deux règles dans une même case — sinon le choix serait indécidable avec un seul symbole d'avance. Une telle grammaire est dite LL(1). Ce n'est pas « une règle par variable » (une variable peut avoir plusieurs règles, départagées par la prévision), ni « un seul passage » (le L de gauche assure déjà la lecture unique de gauche à droite).

Les outils : Lex/Flex et Yacc/Bison

On n'écrit plus un lexer ni un analyseur syntaxique entièrement à la main : on les engendre à partir de leur description, ce qui est l'aboutissement pratique de toute la théorie du cours.

Les deux se complètent : Flex produit le flot d'unités, Bison le consomme pour construire l'arbre. Le point à saisir n'est pas la syntaxe de ces outils, mais ce qu'ils prouvent : les objets abstraits de l'année — automates, expressions régulières, grammaires — sont effectifs au point d'être compilés en programmes par d'autres programmes. La théorie des langages n'est pas une curiosité formelle ; c'est l'ingénierie qui fait tenir chaque compilateur, chaque interpréteur, chaque validateur de format.

À vous

Le TP qui clôt le cours : écrire un analyseur lexical pour un mini-langage d'expressions. Vous découpez prix * 12 + (tva * 20) en unités — identifiants, nombres, opérateurs, parenthèses — selon la règle du plus long.

En le codant, deux choses deviennent évidentes. D'abord, le lexer est un automate fini : chaque branche est un état, la boucle « lire tant que c'est un chiffre » est la boucle d'un AFD. Ensuite, où s'arrête le régulier : le lexer produit des unités mais ne vérifie pas que les parenthèses sont équilibrées — cela demande une pile, donc l'analyse syntaxique du bloc III. Le TP fait ainsi se toucher les deux étages de la hiérarchie, et donne à toute l'année sa raison d'être.

Exercice de code

Écrivez l'analyseur lexical d'un mini-langage d'expressions : découpez le texte en unités (nombres, identifiants, opérateurs, parenthèses) selon la règle du plus long. Reconnaissez le lexer comme un automate fini, et voyez où s'arrête le régulier et où commence l'analyse syntaxique.

Point de départ

// Un analyseur lexical (lexer) transforme un TEXTE en une suite d'UNITÉS
// lexicales (tokens) : nombres, identifiants, opérateurs, parenthèses. C'est
// la première étape de tout compilateur, et c'est un AUTOMATE FINI — donc du
// langage régulier (chapitres 3 à 6).
//
// Chaque type d'unité est décrit par une expression régulière :
//   NOMBRE      : [0-9]+
//   IDENT       : [a-z]+
//   OP          : + - * /
//   PAR_G PAR_D : ( )
//   (les espaces séparent mais ne produisent pas d'unité)

// ── À VOUS : le lexer ───────────────────────────────────────────────────────
// Parcourir 'texte' de gauche à droite et produire la liste des unités.
// Règle du PLUS LONG : "123" est UN nombre, pas trois. On lit donc autant de
// chiffres consécutifs que possible avant de conclure (comme un AFD qui reste
// dans son état "nombre" tant qu'il lit des chiffres).
function estChiffre(c) { return c >= "0" && c <= "9"; }
function estLettre(c)  { return c >= "a" && c <= "z"; }

function analyser(texte) {
  const unites = [];
  let i = 0;
  while (i < texte.length) {
    const c = texte[i];
    if (c === " ") { i++; continue; }              // espace : on saute
    if (estChiffre(c)) {
      // à compléter : lire TOUS les chiffres consécutifs -> une unité NOMBRE
      // puis avancer i
    } else if (estLettre(c)) {
      // à compléter : lire toutes les lettres consécutives -> une unité IDENT
    } else if ("+-*/".includes(c)) {
      // à compléter : une unité OP d'un seul caractère
    } else if (c === "(" || c === ")") {
      // à compléter : une unité PAR
    } else {
      unites.push({ type: "ERREUR", valeur: c }); i++;  // symbole inconnu
    }
  }
  return unites;
}

// ── Vérification ────────────────────────────────────────────────────────────
const src = "prix * 12 + (tva * 20)";
for (const u of analyser(src)) {
  console.log(u.type.padEnd(8) + " " + u.valeur);
}

Solution

function analyser(texte) {
  const unites = [];
  let i = 0;
  while (i < texte.length) {
    const c = texte[i];
    if (c === " ") { i++; continue; }
    if (estChiffre(c)) {
      let j = i;
      while (j < texte.length && estChiffre(texte[j])) j++;   // plus long
      unites.push({ type: "NOMBRE", valeur: texte.slice(i, j) });
      i = j;
    } else if (estLettre(c)) {
      let j = i;
      while (j < texte.length && estLettre(texte[j])) j++;     // plus long
      unites.push({ type: "IDENT", valeur: texte.slice(i, j) });
      i = j;
    } else if ("+-*/".includes(c)) {
      unites.push({ type: "OP", valeur: c }); i++;
    } else if (c === "(" || c === ")") {
      unites.push({ type: "PAR", valeur: c }); i++;
    } else {
      unites.push({ type: "ERREUR", valeur: c }); i++;
    }
  }
  return unites;
}
// Sortie sur "prix * 12 + (tva * 20)" :
//   IDENT prix | OP * | NOMBRE 12 | OP + | PAR ( | IDENT tva | OP * |
//   NOMBRE 20 | PAR )

// ── Ce que ce TP noue, et pourquoi il clôt le cours ─────────────────────────
//
// 1. Le lexer EST un automate fini. Chaque branche du while correspond à un
//    état ; la boucle interne « lire tant que c'est un chiffre » est la
//    boucle d'un AFD sur son état « nombre ». La règle du PLUS LONG est le
//    comportement standard : on reste dans l'état tant que la transition
//    existe. C'est pourquoi les outils réels COMPILENT des expressions
//    régulières en AFD (chapitres 5 et 4) : concis à écrire, rapide à exécuter.
//
// 2. Le lexer produit des UNITÉS ; il ne vérifie PAS la structure. Que
//    « 12 + » soit incomplet, que les parenthèses soient déséquilibrées, ce
//    n'est pas son travail. Vérifier « prix * ( ... ) » bien formé demande de
//    compter les parenthèses — donc une PILE, donc l'analyse SYNTAXIQUE
//    (grammaires et automates à pile, chapitres 7-8).
//
// 3. La chaîne complète d'un compilateur, en une phrase : le texte passe par
//    l'analyse LEXICALE (régulier, ce lexer) qui le découpe en unités, puis
//    par l'analyse SYNTAXIQUE (algébrique) qui en construit l'arbre — celui
//    dont le chapitre 7 disait qu'il porte le sens. Les deux étages de la
//    hiérarchie de Chomsky travaillent l'un après l'autre. Tout le cours
//    converge ici.

Ce que ce cours vous laisse

Vous avez remonté la hiérarchie de Chomsky par le bas, en construisant à chaque étage le même triptyque : une manière de décrire les langages, une machine pour les reconnaître, et un lemme de pompage pour marquer la frontière.

Deux idées survivront à l'oubli des constructions. La première : à chaque classe de langages correspond une quantité précise de mémoire — aucune pour le régulier, une pile pour l'algébrique, un ruban illimité pour la machine de Turing au-dessus. La seconde : ces objets sont effectifs. Ce que vous avez prouvé au tableau, un outil comme Flex le compile en programme — et c'est ce qui fait de la théorie des langages l'un des ponts les plus directs entre les mathématiques et le logiciel qui tourne.

À retenir

Flashcards · 4 cartes

Quelles sont les deux grandes phases de l'analyse d'un programme, et à quelle classe de langages chacune correspond-elle ?
L'analyse LEXICALE découpe le texte en unités (nombres, identifiants, opérateurs) : c'est du RÉGULIER, un automate fini (chapitres 3-6). L'analyse SYNTAXIQUE vérifie la structure et construit l'arbre de dérivation : c'est de l'ALGÉBRIQUE, un automate à pile sur une grammaire hors contexte (chapitres 7-8). Le lexer produit le flot d'unités, l'analyseur syntaxique le consomme.
Qu'est-ce que la règle du plus long dans un analyseur lexical, et à quoi correspond-elle côté automate ?
Face à plusieurs découpages possibles, le lexer lit l'unité la PLUS LONGUE : « 123 » est un seul nombre, « <= » un seul opérateur. Cela correspond au comportement d'un AFD qui reste dans son état tant que la transition existe (boucle sur l'état « nombre » tant qu'il lit des chiffres). C'est pourquoi les lexers sont engendrés à partir d'expressions régulières compilées en AFD.
Que signifie LL(1), et quelle condition la grammaire doit-elle vérifier ?
Lecture de gauche à droite, dérivation gauche (Leftmost), 1 symbole de prévision : l'analyseur construit l'arbre du haut vers le bas en ne regardant qu'UNE unité à l'avance. La grammaire doit être telle que la table d'analyse (variable × unité suivante) n'ait jamais deux règles dans une même case — sinon le choix serait indécidable avec un seul symbole d'avance. Sa construction repose sur les ensembles Premier et Suivant.
Que font Lex/Flex et Yacc/Bison, et que prouvent-ils sur la théorie du cours ?
Flex engendre un analyseur LEXICAL (un AFD) à partir d'expressions régulières ; Bison engendre un analyseur SYNTAXIQUE (un automate à pile) à partir d'une grammaire. Ils prouvent que les objets abstraits du cours sont EFFECTIFS : automates, expressions régulières et grammaires sont compilables en programmes par d'autres programmes. La théorie des langages est l'ingénierie qui fait tenir tout compilateur.

Exercices d'entraînement

Exercice 1

Analyse lexicale

Découper la chaîne 12+34*5 en unités lexicales (nombres, opérateurs). Expliquer, à l'aide de la règle du plus long, pourquoi 12 est une seule unité et non deux.

Correction

La suite d'unités est : NOMBRE(12)  OP(+)  NOMBRE(34)  OP()  NOMBRE(5).\text{NOMBRE}(12)\ \ \text{OP}(+)\ \ \text{NOMBRE}(34)\ \ \text{OP}(*)\ \ \text{NOMBRE}(5).

12 forme une unité parce que l'analyseur applique la règle du plus long : arrivé sur 1, il continue de lire tant que les caractères prolongent un nombre valide, donc il absorbe aussi 2, et ne s'arrête qu'au +. C'est exactement le comportement d'un AFD qui reste dans son état « nombre » tant que la transition sur un chiffre existe. Découper en 1 puis 2 produirait deux nombres accolés, ce qui n'a pas de sens lexical.

Exercice 2

Arbre syntaxique non ambigu

Avec la grammaire désambiguïsée des expressions EE+TT,TTFF,F(E)nombre,E \to E + T \mid T, \qquad T \to T * F \mid F, \qquad F \to (E) \mid \text{nombre}, décrire l'arbre de dérivation de 1+2*3 et donner la valeur calculée.

Correction

La grammaire place l'addition au-dessus de la multiplication, ce qui impose la priorité du *. L'arbre a pour racine la règle EE+TE \to E + T :

  • la branche gauche EE se réduit à 1 (via ETFnombreE \to T \to F \to \text{nombre}) ;
  • la branche droite TT se dérive en TFT * F, donc 2 * 3 (via TTFT \to T*F, avec TFnombre=2T \to F \to \text{nombre} = 2 et Fnombre=3F \to \text{nombre} = 3).

L'arbre représente donc 1 + (2 * 3), et la valeur calculée est 1+6=71 + 6 = 7. Un seul arbre est possible : la grammaire n'est plus ambiguë, et la priorité est encodée dans sa structure.

Exercice 3

Grammaire LL(1) ?

La grammaire SaSbεS \to a\,S\,b \mid \varepsilon est-elle LL(1) ? Justifier. Pourquoi une règle récursive à gauche comme EE+TE \to E + T pose-t-elle problème à une analyse LL(1) ?

Correction

Oui, SaSbεS \to aSb \mid \varepsilon est LL(1). Avec un seul symbole de prévision, le choix de règle est sans ambiguïté :

  • si la prochaine unité est a, on applique SaSbS \to aSb (les dérivations de cette règle commencent par a) ;
  • si c'est b ou la fin du mot, on applique SεS \to \varepsilon.

Les deux cas ne se recoupent pas, donc la table d'analyse n'a aucun conflit.

La récursivité à gauche EE+TE \to E + T est au contraire fatale à LL(1) : pour décider d'appliquer cette règle, l'analyseur devrait d'abord analyser un EE… qui recommence par la même règle, indéfiniment, sans jamais consommer de symbole. Avec un seul symbole d'avance, il ne peut pas trancher. On lève l'obstacle en réécrivant la grammaire sous forme récursive à droite avant l'analyse descendante.