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

Compilation · C3 Analyse syntaxique · Chapitre 3 · 8 h

Analyse ascendante

Décalage-réduction ; items et automate LR(0) ; SLR(1), LR(1) canonique, LALR(1) ; conflits décalage/réduction et réduction/réduction ; Bison ; LL contre LR.

L'analyse descendante prédit les règles depuis l'axiome. L'analyse ascendante prend le problème à l'envers : elle construit l'arbre par le bas, depuis les feuilles, en reconnaissant des membres droits de règles à mesure qu'ils apparaissent, pour les réduire à leur membre gauche. Plus puissante que la descendante — elle accepte la récursivité gauche et une classe de grammaires bien plus large — elle est le moteur de Bison et de la plupart des générateurs sérieux.

C'est aussi le chapitre le plus exigeant, et le point qui coince du cours : la construction des tables LR. Items, fermetures, transitions restent abstraits tant qu'on n'a pas déroulé un automate entier à la main sur une grammaire minuscule. L'exercice — et une feuille de papier — sont ici indispensables.

Décalage-réduction

L'analyse ascendante manipule une pile et effectue, à chaque étape, l'une de deux actions :

On répète jusqu'à réduire toute l'entrée à l'axiome : le programme est alors reconnu. C'est la stratégie décalage-réduction. Sur x avec la grammaire S → x : on décale x, on reconnaît le membre droit de la règle 2, on réduit x en S. Terminé.

Toute la difficulté tient en une question : à chaque instant, faut-il décaler ou réduire, et si on réduit, par quelle règle ? Répondre demande de savoir « où l'on en est » dans la reconnaissance de chaque règle possible — c'est le rôle des items.

Items et automate LR(0)

Un item LR(0) est une règle munie d'un point qui marque jusqu'où le membre droit a été reconnu :

S → ( · S )     j'ai lu '(', j'attends un S, puis ')'S → x ·         j'ai reconnu tout le membre droit : je peux réduire

Un point devant un terminal appelle un décalage ; un point à la fin signale une réduction. On regroupe les items en états, et deux opérations construisent l'automate — exactement les analogues LR de ce que vous connaissez :

Les états sont des ensembles d'items, engendrés par fermetures et transitions successives depuis l'item de départ — exactement comme la déterminisation construisait des ensembles d'états en Théorie des langages. Le résultat est l'automate LR(0), qui reconnaît les préfixes viables de la grammaire. C'est cet automate que l'exercice vous fait construire, closure comprise.

Quiz · 1 question

Que représente l'item LR(0) « S → ( · S ) », et que signale un item dont le point est tout à la fin, comme « S → x · » ?

  • Une erreur de syntaxe ; un état initialerreur/initial
  • Un état où l'on a lu '(' et attend un S puis ')' ; un item « point à la fin » signale qu'un membre droit complet est reconnu, donc une réduction possibleprogression et réduction
  • Une règle ambiguë ; une règle inutileambiguïté

Réponse : Le point marque la progression dans la reconnaissance d'un membre droit : « S → ( · S ) » veut dire « j'ai décalé '(', j'attends maintenant de reconnaître un S, puis de décaler ')' ». Quand le point atteint la fin — « S → x · » — le membre droit entier a été reconnu sur la pile : on est dans un état de RÉDUCTION, on peut remplacer x par S. Un point devant un terminal appellerait au contraire un décalage.

SLR(1), LR(1) canonique, LALR(1)

L'automate LR(0) ignore le contexte droit : il réduit dès qu'il voit un item complet, sans regarder le token suivant. Cela suffit rarement. On raffine en ajoutant un token de prévision (lookahead), d'où une famille de méthodes de puissance croissante :

MéthodeIdéeCompromis
LR(0)réduit sans regarder l'entréetrop faible en pratique
SLR(1)ne réduit A → β que si le token suivant est dans SUIVANT(A)simple, mais rejette des grammaires courantes
LR(1) canoniqueprévision calculée précisément par itemle plus puissant, mais beaucoup d'états
LALR(1)fusionne les états LR(1) à même cœurpresque aussi puissant, tables compactes

LALR(1) est le compromis retenu par Bison et Yacc : il capture la quasi-totalité des grammaires de langages réels avec des tables de taille raisonnable. SUIVANT, croisé au chapitre 4, réapparaît ici comme la prévision de la méthode SLR — les deux mondes, descendant et ascendant, partagent leurs ingrédients de base.

Les conflits

Quand l'automate ne peut pas décider, il y a conflit — et c'est précisément ce que signale un message d'erreur de Bison :

L'essentiel à comprendre : un conflit n'est pas un bug de l'outil, c'est l'automate qui constate qu'un token d'avance ne suffit pas à trancher. Savoir lire un état LR(0) — repérer qu'il contient à la fois une réduction et un décalage possibles — c'est savoir d'où vient concrètement le message.

Bison, et LL contre LR

Bison (successeur libre de Yacc) engendre un analyseur ascendant LALR(1) à partir d'une grammaire annotée d'actions. On écrit les règles, Bison construit l'automate et les tables, et signale les conflits — qu'il faut alors savoir interpréter. Le choix du cours : construire un automate LR(0) à la main avant de lancer Bison, pour comprendre ce que l'outil calcule.

Le bilan entre les deux grandes familles :

LL (descendant)LR (ascendant)
Construit l'arbrepar le hautpar le bas
Récursivité gaucheinterditeacceptée
Puissanceplus faibleplus forte
À la mainfacile (descente récursive)pénible
OutilrareBison, Yacc

En pratique : LL(1) écrit à la main pour un petit langage, LR/LALR via un outil pour tout le reste.

Quiz · 1 question

Bison signale un « conflit décalage/réduction » sur une grammaire. Qu'est-ce que cela signifie, et pourquoi n'est-ce pas un bug de l'outil ?

  • L'outil est mal configuré ; il faut réinstaller Bisonoutil défaillant
  • Dans un état, l'automate pourrait aussi bien décaler le token courant que réduire par une règle : un token d'avance ne suffit pas à trancher — c'est une propriété de la grammaire, pas de l'outilindécision de l'automate
  • La grammaire contient une récursivité gauche, interdite en LRrécursivité gauche

Réponse : Un conflit décalage/réduction signifie qu'un état de l'automate LR contient à la fois un item complet (réduction possible) et un item dont le point précède le token courant (décalage possible) : l'automate ne peut pas choisir avec la prévision dont il dispose. C'est une caractéristique de la GRAMMAIRE (ambiguïté ou besoin de plus de contexte), que Bison ne fait que constater. La récursivité gauche, elle, est parfaitement acceptée en LR — c'est même un avantage sur LL.

À vous

L'exercice attaque le point qui coince de front : construire l'automate LR(0) d'une grammaire de trois règles. Vous implémentez la fermeture (fermer un ensemble d'items), GOTO engendre la collection canonique des états, et vous repérez les états de réduction (point à la fin) et de décalage.

Faites-le aussi sur papier : c'est le seul moyen de rendre concrets items, fermetures et transitions — et de reconnaître, plus tard, d'où vient un conflit signalé par Bison.

Exercice de code

Construisez l'automate LR(0) d'une grammaire minuscule : implémentez CLOSURE (fermer un ensemble d'items) et laissez GOTO engendrer la collection canonique des états. Repérez les états de réduction (point à la fin) et de décalage — et comprenez d'où viendrait un conflit.

Point de départ

// Grammaire minuscule, augmentée (règle 0). '(' ')' 'x' sont des terminaux,
// S' et S des non-terminaux.
//   0 : S' -> S
//   1 : S  -> ( S )
//   2 : S  -> x
const GRAMMAIRE = [
  { g: "S'", d: ["S"] },
  { g: "S",  d: ["(", "S", ")"] },
  { g: "S",  d: ["x"] },
];
const nonTerminaux = new Set(["S'", "S"]);

// Un ITEM = une règle avec un point. On le note "S -> ( . S )".
function item(i, pos) {
  const r = GRAMMAIRE[i];
  const d = [...r.d]; d.splice(pos, 0, ".");
  return r.g + " -> " + d.join(" ");
}
function apresPoint(it) {
  const t = it.split(" ");
  const k = t.indexOf(".");
  return t[k + 1];   // symbole juste après le point, ou undefined si point à la fin
}

// ── À VOUS : CLOSURE ────────────────────────────────────────────────────────
// Règle : pour tout item « A -> alpha . B beta » avec B non-terminal, ajouter
// tous les items « B -> . gamma » (point au début), et recommencer jusqu'à
// stabilité.
function closure(items) {
  const ens = new Set(items);
  let change = true;
  while (change) {
    change = false;
    for (const it of [...ens]) {
      const B = apresPoint(it);
      if (B && nonTerminaux.has(B)) {
        // à compléter : pour chaque règle GRAMMAIRE[i] dont g === B,
        // ajouter item(i, 0) à 'ens' ; noter change = true si nouveau.
      }
    }
  }
  return [...ens].sort();
}

// GOTO(I, X) : avancer le point sur X dans tous les items où c'est possible,
// puis fermer.
function goto(items, X) {
  const avances = [];
  for (const it of items) {
    if (apresPoint(it) === X) {
      const t = it.split(" "); const k = t.indexOf(".");
      [t[k], t[k + 1]] = [t[k + 1], t[k]];   // échange point et symbole
      avances.push(t.join(" "));
    }
  }
  return avances.length ? closure(avances) : null;
}

// ── Collection canonique des états ──────────────────────────────────────────
const etats = [];
const cle = (I) => I.join(" | ");
const depart = closure([item(0, 0)]);   // closure de { S' -> . S }
etats.push(depart);
const transitions = [];
for (let s = 0; s < etats.length; s++) {
  const symboles = new Set(etats[s].map(apresPoint).filter(Boolean));
  for (const X of symboles) {
    const J = goto(etats[s], X);
    if (!J) continue;
    let idx = etats.findIndex((E) => cle(E) === cle(J));
    if (idx < 0) { etats.push(J); idx = etats.length - 1; }
    transitions.push([s, X, idx]);
  }
}

console.log(etats.length + " états LR(0) :\n");
etats.forEach((I, s) => { console.log("I" + s + " :"); I.forEach((it) => console.log("   " + it)); });
console.log("\ntransitions :");
transitions.forEach(([a, X, b]) => console.log("   I" + a + " --" + X + "--> I" + b));

Solution

function closure(items) {
  const ens = new Set(items);
  let change = true;
  while (change) {
    change = false;
    for (const it of [...ens]) {
      const B = apresPoint(it);
      if (B && nonTerminaux.has(B)) {
        for (let i = 0; i < GRAMMAIRE.length; i++) {
          if (GRAMMAIRE[i].g === B) {
            const nouv = item(i, 0);
            if (!ens.has(nouv)) { ens.add(nouv); change = true; }
          }
        }
      }
    }
  }
  return [...ens].sort();
}

// ── Résultat (5 états) ──────────────────────────────────────────────────────
//  I0 : S' -> . S | S -> . ( S ) | S -> . x
//  I1 : S' -> S .                          (GOTO I0 sur S)
//  I2 : S -> ( . S ) | S -> . ( S ) | S -> . x   (GOTO I0 sur '(')
//  I3 : S -> x .                           (GOTO I0 sur x)
//  I4 : S -> ( S . )                        (GOTO I2 sur S)
//  I5 : S -> ( S ) .                        (GOTO I4 sur ')')
//
// ── Ce que l'exercice enseigne (le point qui coince) ────────────────────────
//
// 1. Un ITEM est une règle avec un POINT marquant « jusqu'où on a reconnu ».
//    « S -> ( . S ) » signifie : j'ai lu '(', j'attends un S, puis ')'.
//
// 2. La CLOSURE rend explicite ce qu'on peut commencer à reconnaître : si le
//    point précède un non-terminal B, alors TOUTES les règles de B peuvent
//    démarrer ici, donc on ajoute leurs items « point au début ». C'est
//    l'équivalent LR de l'ensemble PREMIER.
//
// 3. GOTO(I, X) = avancer le point sur X, puis fermer. C'est la TRANSITION de
//    l'automate : les états sont des ENSEMBLES d'items, exactement comme la
//    déterminisation transformait des ensembles d'états (Théorie des langages).
//
// 4. Un item « point à la fin » (S -> x .) est un état de RÉDUCTION : on a
//    reconnu tout un membre droit, on peut le remplacer par le membre gauche.
//    Un item « point devant un terminal » appelle un DÉCALAGE (shift).
//    Ici aucun état ne mélange les deux de façon ambiguë : la grammaire est
//    LR(0). Quand un état contient à la fois une réduction et un décalage
//    possible sur le même symbole, c'est un CONFLIT décalage/réduction — le
//    message d'ambiguïté que crache Bison.
//
// Construire cet automate À LA MAIN, sur trois règles, est le seul moyen de
// rendre concrets items, fermetures et transitions. Faites-le sur papier
// aussi.

Ce que la suite en fait

Le bloc III est complet : à partir d'une suite de tokens, vous savez construire l'arbre syntaxique, par le haut ou par le bas. Mais un arbre bien formé n'est pas encore un programme correct : x = y + 1 est syntaxiquement impeccable même si y n'a jamais été déclarée, ou si y est une chaîne.

Le bloc IV donne un sens à l'arbre. Le chapitre 6 construit la table des symboles — qui suit les déclarations et les portées — et le chapitre 7 s'en sert pour la vérification de types. C'est là que les erreurs sémantiques du chapitre 1, invisibles à l'analyse syntaxique, sont enfin détectées.

À retenir

Flashcards · 4 cartes

En quoi l'analyse ascendante diffère-t-elle de la descendante ?
Elle construit l'arbre par le BAS (des feuilles vers l'axiome) en RECONNAISSANT des membres droits de règles, par décalage-réduction : on empile les tokens (décalage), et dès que le sommet de la pile forme un membre droit A → β, on le remplace par A (réduction). Plus puissante que la descendante (elle accepte la récursivité gauche), mais moins intuitive à écrire à la main. C'est le moteur de Bison.
Qu'est-ce qu'un item LR(0), une fermeture (closure) et une transition GOTO ?
Un item est une règle avec un POINT marquant jusqu'où le membre droit est reconnu (S → ( · S )). La CLOSURE : si le point précède un non-terminal B, ajouter tous les items « B → · γ » (analogue de PREMIER). GOTO(I, X) : avancer le point sur X dans tous les items possibles, puis fermer (la transition). Les états sont des ENSEMBLES d'items — comme la déterminisation construisait des ensembles d'états.
Que sont SLR(1), LR(1) canonique et LALR(1), et lequel Bison emploie-t-il ?
Des raffinements de LR(0) par un token de prévision. SLR(1) : réduit A → β seulement si le token suivant est dans SUIVANT(A) (simple mais limité). LR(1) canonique : prévision précise par item (puissant, beaucoup d'états). LALR(1) : fusionne les états LR(1) à même cœur (presque aussi puissant, tables compactes) — c'est le choix de Bison et Yacc.
Qu'est-ce qu'un conflit décalage/réduction, et pourquoi n'est-ce pas un bug de l'outil ?
Un état où l'automate pourrait aussi bien décaler le token courant que réduire par une règle : la prévision ne suffit pas à trancher (ex. le « dangling else »). C'est une propriété de la GRAMMAIRE que Bison constate, pas un défaut de l'outil ; il le résout par défaut en décalant. Un conflit réduction/réduction (deux règles réductibles) est plus grave et révèle une grammaire mal conçue.