Compilation/Analyse syntaxique

Leçons de niveau 17
Une page de Wikiversité, la communauté pédagogique libre.
Début de la boite de navigation du chapitre
Analyse syntaxique
Icône de la faculté
Chapitre no 3
Leçon : Compilation
Chap. préc. :Analyse lexicale
Chap. suiv. :Analyse sémantique
fin de la boite de navigation du chapitre
En raison de limitations techniques, la typographie souhaitable du titre, « Compilation : Analyse syntaxique
Compilation/Analyse syntaxique
 », n'a pu être restituée correctement ci-dessus.

Rôles de l'analyse syntaxique[modifier | modifier le wikicode]

Cette phase à plusieurs objectifs :

  • déterminer si la suite de tokens est conforme à la grammaire définissant le langage source ;
  • repérer les erreurs de nature syntaxique ;
  • définir une structure hièrarchique (un arbre).

En pratique, d'autres tâches sont réalisées simultanément pendant l'analyse syntaxique :

  • ajouter des informations à la table des symboles ;
  • effectuer des vérifications de type ;
  • produire du code intermédiaire.

Méthodes d'analyse syntaxique[modifier | modifier le wikicode]

La plupart des méthodes d'analyse syntaxique tombent dans deux classes :

  1. les méthodes d'analyse descendantes (top-down parsing(en)) sont les plus populaires car les analyses correspondants sont faciles à mettre en œuvre à la main ;
  2. les méthodes d'analyse ascendantes (bottom-up parsing(en)) sont les plus puissantes et sont celles utilisées par les outils qui génèrent automatiquement un analyseur à partir d'une grammaire (exemple : yacc).

Pour illustrer les différences entre ces méthodes nous utilisons l'exemple du langage des expressions arithmétiques.

Ce langage peut être décrit par la grammaire algébrique suivante :

E → E + T | T
T → T × F | F
F → ( E ) | id

Note : par convention, les symboles en capitale comme E, T et F sont des non-terminaux et les autres symboles comme +, ×, (, ) et id sont des terminaux (les identifiants des tokens).

Dans la suite de la leçon nous allons considérer l'analyse de la chaîne ( id + id ) * id avec les deux approches méthodologiques précédemment cités..

Analyses descendantes[modifier | modifier le wikicode]

Dans les analyses descendantes :

  • l'arbre est construit en partant de la racine et en allant jusqu'au feuilles ;
  • l'analyse s'appuie sur une dérivation gauche de la chaîne, c'est-à-dire qu'on remplace toujours en premier le non-terminal le plus à gauche.

On présente ci-dessous la dérivation de la chaîne depuis la racine et l'arbre d'analyse qui en résulte. Pour bien faire resortir le parallèle entre les deux on y numérote chaque étape.

E (Étape 0)
E ⇒g T (Étape 1)
g T * F (Étape 2)
g (E) * F (Étape 3)
g (E+T) * F (Étape 4)
g (T+T) * F (Étape 5)
g (F+T) * F (Étape 6)
g (id+T) * F (Étape 7)
g (id+F) * F (Étape 8)
g (id+id) * F (Étape 9)
g (id+id) * id (Étape 10)
E (Étape 0)
│
│
└──T (Étape 1) 
   │
   ├──F (Étape 2)
   │  │
   │  └──id (Étape 10)
   │
   ├──* (Étape 2)
   │
   └──T (Étape 2)
      │
      ├──) (Étape 3)
      │
      ├──E (Étape 3)
      │  │
      │  ├──T (Étape 4)
      │  │  │
      │  │  └──F (Étape 8)
      │  │     │
      │  │     └──id (Étape 9)
      │  │ 
      │  ├──+ (Étape 4)
      │  │
      │  └──E (Étape 4)
      │     │
      │     └──T (Étape 5)
      │        │
      │        └──F (Étape 6)
      │           │
      │           └──id (Étape 7)
      │
      │
      │
      │
      └──( (Étape 3)

À chaque étape, on sélectionne une règle qui permet de dériver la chaîne. Cela peut entraîner des développement d'impasse et donc nécessiter retours arrières (ce n’est pas le cas dans notre exemple ou nous avons fait les bons choix).

On peut limiter les retours arrières en utilisant l'information des premiers symboles terminaux qui dérivent d'une règle.

Pour les analyses ascendantes[modifier | modifier le wikicode]

On agit inversement :

  • l'arbre est construit en remontant à la racine depuis les feuilles ;
  • l'analyse s'appuie sur une dérivation droite de la chaîne c'est-à-dire qu'on remplace toujours en premier le non-terminal le plus à droite.
(id+id)*id (Étape 0)
d(F+id)*id (Étape 1)
d(T+id)*id (Étape 2)
d(E+id)*id (Étape 3)
d(E+F)*id (Étape 4)
d(E+T)*id (Étape 5)
d(E)*id (Étape 6)
dF*id (Étape 7)
dT*id (Étape 8)
dT*F (Étape 9)
dT (Étape 10)
dE (Étape 11)
( id + id ) * id (Étape 0)
│ │ │ │ │ │ │
│ F │ │ │ │ │ (Étape 1)
│ │ │ │ │ │ │
│ T │ │ │ │ │ (Étape 2)
│ │ │ │ │ │ │
│ E │ │ │ │ │ (Étape 3)
│ │ │ │ │ │ │
│ │ │ F │ │ │ (Étape 4)
│ │ │ │ │ │ │
│ │ │ T │ │ │ (Étape 5)
│ │ │ │ │ │ │
│ └─┼──┘ │ │ │
│ E │ │ │ (Étape 6)
│ │ │ │ │
└────┼────┘ │ │
     │      │ │
     F      │ │    (Étape 7)
     │      │ │
     T      │ │    (Étape 8)
     │      │ │
     │      │ F    (Étape 9)
     │      │ │
     └──────┼─┘
            │
            T      (Étape 10)
            │
            E      (Étape 11)

Note : dans l'arbre ci-dessus les longueur des branches ne sont pas significatives, elles font resortir les étapes de façon commode mais arbitraire. L'arbre suivant est tout à fait équivalent :

( id + id ) * id 
│ │ │ │ │ │ │
│ F │ F │ │ F 
│ │ │ │ │ │ │
│ T │ T │ │ │
│ │ │ │ │ │ │
│ E │ │ │ │ │ 
│ └─┼──┘ │ │ │
│ │ │ │ │
│ E │ │ │
└────┼────┘ │ │
     │      │ │
     F      │ │ 
     │      │ │
     T      │ │
     └──────┼─┘
            │
            T
            │
            E


À chaque étape, on sélectionne une partie au début de la chaîne qui dérive d'un non-terminal et on le remplace par ce non-terminal.

L'inverse de la suite des règles utilisées est une dérivation droite de la chaîne.

On constate que nos algorithmes sont imprécis, ils demandent de faire des choix, ils sont non-déterministe.

Analyse syntaxique descendante[modifier | modifier le wikicode]

Exemple[modifier | modifier le wikicode]

Considérons la grammaire suivante : S→cAd A→ab|a

Analysons la chaîne « cad » et construisons l'arbre correspondant.

Étape Arbre Explication
0
S           cad
↑            ↑
S est la racine de notre arbre, cad notre chaîne et les flèches (↑) représentent la position de deux pointeur, ici sur le S de l'arbre et le c du début de la chaîne.
1
   S           cad
 ┌─┼─┐         ↑
 c A d         │
 ↑             │
 └─────────────┘ 
  • le pointeur de l'arbre pointe sur un non-terminal, on développe l'arbre à cette position d’après la grammaire ;
  • on positionne le pointeur de l'arbre sur le terminal extrême gauche dans l'arbre ;
  • on compare les deux symboles pointés respectivement dans l'arbre et la chaîne ;
  • ici les deux symboles correspondent, on avance les deux pointeurs.
2
   S           cad
 ┌─┼─┐          ↑
 c A d          │
  ┌┴┐           │
  a b           │
  ↑             │
  └─────────────┘ 
* le pointeur de l'arbre pointe sur un non-terminal, on développe l'arbre à cette position d’après la grammaire, qui ici permet plusieurs possibilité, on choisi le premier qu'elle propose ;
  • on positionne le pointeur de l'arbre sur le terminal extrême gauche dans l'arbre ;
  • on compare les deux symboles pointés respectivement dans l'arbre et la chaîne ;
  • ici les deux symboles correspondent, on avance les deux pointeurs.
3
   S           cad
 ┌─┼─┐           ↑
 c A d           │
  ┌┴┐            │
  a b            │
    ↑            │
    └────────────┘ 
  • on compare les deux symboles pointés respectivement dans l'arbre et la chaîne ;
  • les deux symboles diffèrent ;
  • le choix de développement par A→ab à l'étape 2 n'était pas bon ;
  • on « dépile » le précédent développement.
2’
   S           cad
 ┌─┼─┐          ↑
 c A d          │
   │            │
   a            │
   ↑            │
   └────────────┘ 
* les pointeurs sont replacés comme en fin d'étape 1 et on développe le non-terminal avec la règle alternative A→a ;
  • on positionne le pointeur de l'arbre sur le terminal extrême gauche dans l'arbre ;
  • on compare les deux symboles pointés respectivement dans l'arbre et la chaîne ;
  • ici les deux symboles correspondent, on avance les deux pointeurs.
3’
   S           cad
 ┌─┼─┐           ↑
 c A d           │
   │ ↑           │
   a └───────────┘
  • on compare les deux symboles pointés respectivement dans l'arbre et la chaîne ;
  • ici les deux symboles correspondent, on est arrivé au bout de la chaîne, on à donc terminé avec succès.

Algorithme naïf[modifier | modifier le wikicode]

Décrivons plus formellement l'algorithme que nous venons d’utiliser :

  • à chaque étape, on examine le nœud courant de l'arbre en parcourant les feuilles de gauche à droite ;
  • si le nœud courant de l'arbre est un non-terminal A, on sélectionne une règle avec A en partie gauche ;
    • si cette règle est A→ε (où ε désigne le mot vide), on passe au nœud suivant de l'arbre ;
    • sinon on développe le nœud courant avec la règle ;
  • si le nœud courant est un terminal, on le compare avec l'élément courant de la chaîne:
    • s'il y a égalité, on passe au nœud suivant et on avance le pointeur dans la chaîne ;
    • sinon on reviens au dernier choix de règle, ce qu'on nomme « retour arrière » ou « dépilage ».

Problèmes rencontrés avec l'algorithme naïf[modifier | modifier le wikicode]

Un retour-arrière coûteux[modifier | modifier le wikicode]

Les retours-arrière sont coûteux en termes de calcul. Ils sont dus à la présence de grammaire avec un même début en partie droite, de la forme : .

Pour ces règles on ne sait pas choisir sans risque d'erreur.

La solution consiste à éliminer ces règles par substitution. Ainsi sera remplacé par .

Bouclages infinis[modifier | modifier le wikicode]

Des grammaires peuvent conduire à des phénomènes de bouclage infini, dus à la présence de règles de grammaire récursives gauche, de la forme :

  • (récursivité directe) ;
  • et (récursivité indirecte) ;

La solution consiste à remplacer ces règles en supprimant la récursivité à gauche. Cela ne pose pas de problème puisque nous disposons d'un théorème qui dit que :

Début d’un théorème
Fin du théorème


Le principe de remplacement fonctionne ainsi :

  • en cas de récursivité directe sera remplacé par et

  • en cas de récursivité indirecte
remplacé par

L'algorithme comporte des opérations coûteuses[modifier | modifier le wikicode]

En effet la recherche d'une règle de grammaire, les opérations de manipulation, le parcours de l'arbre, sont autant d'opérations rallongeant le temps de calcul.

Deux solutions sont possible pour résoudre ces problèmes :

  1. utiliser l'analyse par descente récursive ;
  2. utiliser l'analyse itérative, LL(1) (ce terme sera expliqué plus loin dans le cours).

Analyse par descente récursive[modifier | modifier le wikicode]

En utilisant cette méthode que nous avons précédemment présenté, on peut associer une procédure à chaque non-terminal.

procédure S
   match(c);A;match(d)
end
procédure A
   match(a);if curent_token = b then match(b)
end
procédure B
   if curent_token = t then curent_token = next_token
                       else error
end
analyse d'une chaîne :
    initialement
       curent_token = first_token
       call S

La suite des appels de procédure définit l'arbre d'analyse. Dans le cas général, les procédures sont mutuellement récursives.

Il nous faut passer à une analyse itérative, par exemple LL(1). Pour ce faire on utilise une pile, ce qui permet de supprimer la récursivité.


Analyse itérative, LL(1)[modifier | modifier le wikicode]

Cette méthode consiste à utiliser une pile, ce qui permet de supprimer la récursivité.

Cette méthode est ainsi nommée car :

  • l'analyse de la chaîne s'effectue de gauche à droite (le L venant de (en), gauche en anglais) ;
  • on construit un dérivation gauche ;
  • on lit 1 token en avance (on parle alors d'analyse prédictive).


Principe de l'analyse LL[modifier | modifier le wikicode]

Cette algorithme utilise :

  • un pointeur sur l'élément courant dans la chaîne à analyser (sufixée par , le marqueur de fin) ;
  • une pile qui peut contenir des terminaux, des non-terminaux ou  ;
  • une table d'analyse , indexée par :
    • les non-terminaux (lignes) ;
    • les terminaux ∪ (chaînes).

Chaque élément contient la règle de grammaire à utiliser lorsque A est l'élément courant de la chaîne.

Initialement, le pointeur pointe sur le premier élément de la chaîne et la pile contient et  ; se situant au sommet de la pile.

À chaque itération, l'analyseur examine l'élément au sommet de la pile et l'élément courant de la chaîne et réalise le traitement suivant :

  • si est un terminal ou  :
    • si , terminer avec succès ;
    • si , dépiler et avancer le pointeur ;
    • sinon terminer avec erreur ;
  • si est un non-terminal :
    • si contient dépiler et empiler dans cet ordre ;
    • sinon terminer avec erreur.

Exemple[modifier | modifier le wikicode]

Considérons la grammaire obtenue à partir de en supprimant la récursivité gauche.

E → E + T
E → T
T → T × F
T → F
F → ( E ) 
F → id
E → TE'
E' → +TE'|ε
T → FT'
T' → ×FT'|ε
F → ( E ) 
F → id
Une table d'analyse pour cette grammaire est :
id + × ( ) $
E E → TE' E → TE'
E' E' → TE' E' → ε E' → ε
T T → FT' T → FT'
T' T' → ε T' → ×FT' T' → ε T' → ε
F F → id F → ( E )

Note : les cases vides représentes les cas d'erreur (token inattendu)

Utilisons le tableau pour faire l'analyse de la chaîne id + id × id.

Chaîne Pile Règle Arbre
id + id × id $ $ E à faire
id + id × id $ $ E' T E → T E'
id + id × id $ $ E T' F T → FT'
id + id × id $ $ E' T' id F → id
+ id × id $ $ E' T'
+ id × id $ $ E' T' → ε
+ id × id $ $ E' T + E' → +TE'
id × id $ $ E' T
id × id $ $ E' T' F T→FT'
id × id $ $ E' T' id F→id
× id $ $ E' T'
× id $ $ E' T' F × T'→F × FT'
id $ $ E' T' F
id $ $ E' T' id F→id
$ $ E' T'
$ $ E' T' T'→ε
$ $ E' E'→ε
$ $

La table d'analyse peut être remplie en utilisant 2 fonctions associées à la grammaire : les fonctions FIRST et FOLLOW.

Soit α, une chaîne contenant des terminaux ou des non-terminaux.

Soit A, un non terminal

Par définition :

  • FIRST(α) = ensemble de terminaux qui peuvent se trouver au début des chaînes dérivées de α ;
  • FOLLOW(A) = ensemble des terminaux qui peuvent se trouver immédiatement après A dans une chaîne de dérivation.

Construction de la table d'analyse avec FIRST et FOLLOW[modifier | modifier le wikicode]

L'idée est que pour toute règle de grammaire A→α

  • si a ∈ FIRST(α), alors l'analyseur va utiliser cette règle quand l'élément courant dans la chaîne est a. La seule complication est lorsque α=ε ou bien α→ ε dans ce cas on doit également développer A par α.
  • si l'élément courant dans la chaîne appartient à FOLLOW(A)

G = (Vt, Vn, P, S)
Vt: Ensemble des terminaux
Vn: Ensemble des non terminaux
P: Ensemble des règle de production
S: Axiome


pour toute production X → γ ∈ P

 faire pour tout a ∈ First (γ)
   faire ajouter X → γ ` Table[X , a] fait
   si ε ∈ First(γ) alors pour tout b ∈ Follow(X )
     faire Table[X , b] = X → γ
     fait
   finsi

fait

Calcul des ensemble FIRST et FOLLOW[modifier | modifier le wikicode]

Pour un grammaire G=(V,Σ,R,S)

  • FIRST(α) =
  • FOLLOW(A) =

Comment calculer FIRST[modifier | modifier le wikicode]