Chapitre 4 Analyse lexicale et syntaxique ISBN 0
Chapitre 4 Analyse lexicale et syntaxique ISBN 0 -321 -49362 -1
Chapitre 4: Sujets • Introduction • Analyse lexicale • Analyse syntaxique – Méthode descendante – Méthode ascendante 2
Introduction • 3 principales méthodes pour implémenter un langage de programmation: – compilation – interprétation – méthode hybride Dans tous les cas, il est nécessaire de se référer au code source pour détecter les erreurs de syntaxe. • Pratiquement toutes les méthodes d'analyse de la syntaxe reposent sur une description formelle de la syntaxe du langage (grammaire hors-contexte) 3
Analyse syntaxique • Deux parties: – Au niveau inférieur il y a l'analyseur lexical (automates finis) – Au niveau supérieur il y a l'analyseur syntaxique ou parseur (automates à pile) 4
Avantages de la séparation • Simplicité – méthodes moins complexes peuvent être utilisées pour l'analyse lexicale; simplifie le parseur • Efficacité- séparation permet l'optimisation de l'analyseur lexical. • Portabilité – l'analyseur lexical dépend souvent de la plateforme (lecture de fichiers) alors que l'analyseur syntaxique est toujours portable. 6
Analyse lexicale • Agit comme interface pour l'analyseur syntaxique • Identifie les parties du programme source correspondant aux lexèmes – Un lexème est un mot correspondant à une catégorie de mots (token) – Par exemple, la variable somme est un lexème appartenant au token identificateur 7
Analyse lexicale (suite) • L'analyseur syntaxique appelle l'analyseur lexical lorsqu'il a besoin d'un autre token • 3 approches pour construire un analyseur lexical: – Écrire une description formelle des tokens (expressions régulière) et utiliser un outil logiciel (ex. lex) pour transformer ces descriptions en un analyseur lexical. – Concevoir un diagramme d'état pour décrire les tokens et implémenter ce diagramme sous forme de programme. – Concevoir un diagramme d'état pour décrire les tokens et implémenter ce diagramme à l'aide d'une table. 8
Construire un analyseur lexical • Par exemple, si on veut lire trois types de tokens: – identificateurs: lettre suivie de lettres ou de chiffres. – mots clefs: suite de lettres – entiers littéraux: chiffre suivit de chiffres • Approche naïve: Une transition pour chaque état et chaque caractère – trop gros! 9
Construire un analyseur lexical (suite) • Plusieurs transition peuvent être combinées pour simplifier le diagramme – Une seule classe de caractères pour toutes lettres majuscules et minuscule (LETTER) – Une seule classe de caractères pour tous les chiffres (DIGIT) 10
Construire un analyseur lexical (suite) • Plutôt que d'avoir des états distincts pour les mots réservés, on traite ceux-ci de la même manière que les identificateurs – On consulte une table pour déterminer si un lexème est un mot clef 11
Diagramme d'état 12
Construire un analyseur lexical (suite) • Variables globales et utilitaires: char next. Char: contient le dernier caractère lu int char. Class: LETTER (0), DIGIT (1), UNKNOWN (-1) char lexeme[100]: chaîne de caractère get. Char() – lit le prochain caractère et le place dans next. Char. Met la classe du caractère dans char. Class. – add. Char() – Ajoute next. Char à la fin de lexeme – int lookup(char*) – détermine si lexeme est un mot clef – – 13
Implémentation int lex() { get. Char(); switch (char. Class) { case LETTER: add. Char(); get. Char(); while (char. Class == LETTER || char. Class == DIGIT) { add. Char(); get. Char(); } return lookup(lexeme); break; … 14
Implémentation … case DIGIT: add. Char(); get. Char(); while (char. Class == DIGIT) { add. Char(); get. Char(); } return INT_LIT; break; } /* End of switch */ } /* End of function lex */ 15
L'analyseur syntaxique • Objectifs: – Trouver toutes les erreurs de syntaxe et envoyer un message approprié. – Produire l'arbre syntaxique ou, du moins, l'information nécessaire pour le construire. 16
L'analyseur syntaxique (suite) • Deux catégories d'analyseurs syntaxiques: – Descendant – produit l'arbre syntaxique en commençant par la racine – Ascendant – commence par les feuilles Dans les deux cas on utilise une grammaire hors-contexte comme description du langage. 17
L'analyseur syntaxique (suite) • Complexité – À partir de toute grammaire non ambiguë, il est possible de construire un analyseur syntaxique fonctionnant en temps O(n 3), où n est la taille de l'entrée. – On utilise plutôt un type restreint de grammaire permettant d'effectuer l'analyse syntaxique en temps O(n) 18
L'analyseur syntaxique (suite) • Parseurs descendants – On commence avec le symbole de départ et on détermine la séquence de règles nécessaires pour dériver l'entrée (suite de token) <départ> entrée • Algorithme LL: – Left-to-right scan (lecture de l'entrée de gauche à droite) – Leftmost derivation (dérivation par la gauche) 19
L'analyseur syntaxique (suite) • Parseurs ascendants – On commence avec l'entrée et on applique à rebours les règles de la grammaire afin d'obtenir la symbole de départ. entrée <départ> • Algorithme LR: • Left-to-right scan • Rightmost derivation 20
Analyseur récursif-descendant (LL) • Un sous-programme pour chaque non-terminal • Les sous-programmes sont mutuellement récursifs • Les grammaires hors-contextes étendues sont appropriées pour ce type d'analyseur syntaxique car elles minimisent le nombre de sousprogrammes. • On suppose l'existence d'un analyseur lexical lex, qui met le prochain token dans la variable globale next. Token 21
Analyseur récursif-descendant (suite) • Exemple: <expr> <term> {(+ | -) <term>} <term> <factor> {(* | /) <factor>} <factor> id | ( <expr> ) 22
Analyseur récursif-descendant (suite) // // Sous-programme pour le non-terminal expr // void expr() { term(); // Sous-programme pour le non-terminal term while (next. Token == PLUS_CODE || next. Token == MINUS_CODE){ lex(); term(); } } 23
Analyseur récursif-descendant (suite) // // Sous-programme pour le non-terminal term // void term() { factor(); // Sous-programme pour le non-terminal factor while (next. Token == MULT_CODE || next. Token == DIV_CODE){ lex(); factor(); } } 24
Analyseur récursif-descendant (suite) void factor() { if (next. Token) == ID_CODE) lex(); else if (next. Token == LEFT_PAREN_CODE) { lex(); expr(); if (next. Token == RIGHT_PAREN_CODE) lex(); else error(); } else error(); } 25
Analyseur récursif-descendant (suite) • Grammaires LL – Forme particulière • Le problème de la récursion à gauche: A → Ab ou A → Bb B → Ac – De telles grammaires ne peuvent pas être utilisée par un analyseur syntaxique descendant – On peut cependant toujours remplacer une telle grammaire par une autre n'ayant pas ce problème 26
Analyseur récursif-descendant (suite) Autre problème: – Deux règles: A→α et A→β tels que α génère aα' et β génère aβ' – Comment choisir entre ces deux règles? – La grammaire doit être modifiée mais cela n'est pas toujours possible 27
Analyseurs ascendants • Forme sententielle droite: On développe le nonterminal le plus à droite en premier • Exemple: E → E + T | T T → T * F | F F → (E) | id E ⇒ E+T*F ⇒ E+T*id ⇒ E+F*id ⇒ E+id*id ⇒ F+id*id ⇒ id+id*id • On veut partir de id+id*id et remonter vers E 28
Analyseurs ascendants (suite) • Handle: segment d'une forme sententielle (dérivée par la droite) correspondant à la partie à droite de la dernière règle utilisée • Exemple: E ⇒ E+T ⇒ E+F ⇒ E+id ⇒ E+T+id E+T est le handle de E+T+id • Remarque: Si la grammaire est non-ambiguë alors le handle est unique • Problème: Comment trouver le handle? 29
Analyseurs ascendants (suite) • Les algorithmes Shift-Reduce: – Utilisent une pile – Déplacement (Shift): Action de placer le prochain token sur le dessus de la pile – Reduction: Action de remplacer le handle sur le dessus de la pile par la partie de gauche (nonterminal) de la règle correspondante 30
Analyseurs ascendants (suite) • Parseurs LR – Type particulier d'analyseurs ascendants conçus par D. Knuth en 1965 – Ils fonctionnent pour pratiquement toutes les grammaires décrivant des langages de programmation – Ils peuvent détecter les erreurs de syntaxe aussitôt que possible. – Plus généraux que les parseurs LL 31
Analyseurs ascendants (suite) • Un parseur LR est un automate fini augmenté d'une pile • Une configuration de la machine a la forme: (S 0 X 1 S 1 X 2 S 2 … Xm Sm, aiai+1…an$) 1. aiai+1…an : Partie de l'entrée qui reste à lire 2. Si : États de la machine 3. Xi : Symboles de la grammaire 32
Structure d'un parseur LR 33
Analyseurs ascendants (suite) • Le comportement d'un parseur LR est indiqué par deux tables: – la table ACTION (lignes=états, colonnes=terminaux) – la table GOTO (lignes=états, colonnes=non-terminaux) • La table ACTION indique si un déplacement ou une réduction doit avoir lieu étant donné l'état courant et le prochain token (terminal) • La table GOTO indique quel état placer sur le dessus de la pile après qu'une réduction ait eu lieu. 34
Analyseurs ascendants (suite) • Configuration initiale: (S 0, a 1…an$) • Configuration courante: (S 0 X 1 S 1 X 2 S 2…Xm. Sm, aiai+1…an$) • Comportement du parseur: – Si ACTION[Sm, ai] = Shift S, la prochaine configuration sera: (S 0 X 1 S 1 X 2 S 2…Xm. Smai. S, ai+1…an$) – Si ACTION[Sm, ai] = Reduce A et que S = GOTO[Sm-r, A], où r = | |, alors la prochaine configuration sera: (S 0 X 1 S 1 X 2 S 2…Xm-r. Sm-r. AS, aiai+1…an$) 35
Analyseurs ascendants (suite) • Comportement du parseur (suite): – Si ACTION[Sm, ai] = Accept, alors l'analyse est complété et aucune erreur n'a été trouvée. – Si ACTION[Sm, ai] = Error, alors le parseur appelle un utilitaire de gestion des erreurs. 36
Analyseurs ascendants (suite) • Exemple: 1. 2. 3. 4. 5. 6. E→E+T E→T T→T*F T→F F→(E) F → id • Les tables ACTION et GOTO sont habituellement construite à l'aide d'un programme telle que yacc ou bison 37
Les tables ACTION et GOTO 38
Analyseurs ascendants (suite) Pile 0 Entrée Action id + id * id $ Shift 5 0 id 5 + id * id $ Reduce 6 (GOTO[0, F]) 0 F 3 + id * id $ Reduce 4 (GOTO[0, T]) 0 T 2 + id * id $ Reduce 2 (GOTO[0, E]) 0 E 1 + id * id $ Shift 6 id * id $ Shift 5 0 E 1+6 0 E 1 + 6 id 5 * id $ Reduce 6 (GOTO[6, F]) 0 E 1+6 F 3 * id $ Reduce 4 (GOTO[6, T]) 0 E 1+6 T 9 * id $ Shift 7 id $ Shift 5 0 E 1+6 T 9*7 0 E 1 + 6 T 9 * 7 id 5 $ Reduce 6 (GOTO[7, F]) 0 E 1 + 6 T 9 * 7 F 10 $ Reduce 3 (GOTO[6, T]) 0 E 1+6 T 9 $ Reduce 1 (GOTO[0, E]) 0 E 1 $ Accept 1) 2) 3) 4) 5) 6) E→E+T E→T T→T*F T→F F→(E) F → id 39
Comprendre les tables: les items (0) (1) (2) S → T$ T → 0 T 0 T→ 1 Items: S → _T$ T → _0 T 0 S → T_$ T → 0_T 0 S → T$_ T → 0 T_0 T → 0 T 0_ Items complets: S → T$_ T → 0 T 0_ T → 1_ T → _1 T → 1_ 40
Signification des items S S→_T$ T $ S→T_$ S→T$_ 00 T T 00 T→ 0_T 0 T→ 0 T_0 T→ 0 T 0_ 0 0 T→ 0_T 0 • Un item de la forme A→α_β indique l'on est en train de traiter la règle A→αβ alors que α est sur les dessus de la pile • Un item complet de la forme A→γ_ indique γ est sur le dessus de la pile et qu'on peut le remplacer par A (réduction) 1 T→ 1_ 41
Signification des items (suite) Analyse du mot 00100$ S Shifts (3 fois) S→_T$ T $ S→T_$ S→T$_ 00 T T 00 T→ 0_T 0 T→ 0 T_0 T→ 0 T 0_ Reduce Shift Reduce 0, T→ 0 T 0_ T, T→ 0 T_0 1, T→ 1_ T, T→ 0 T_0 0, T→ 0_T 0 S, S→_T$ 0, T→ 0_T 0 S, S→_T$ T, T→ 0 T_0 0, T→ 0_T 0 S, S→_T$ Shift 0 0 T→ 0_T 0 T→ 0 T_0 T→ 0 T 0_ T 1 T→ 1_ Reduce Shift Reduce 0 0, T→ 0 T 0_ T, T→ 0 T_0 0, T→ 0_T 0 $, S→T$_ S, S→_T$ T, S→T_$ S, S→_T$ S, accept 42
Automate fini 0 0 2 0 S→_T$ T T T→ 0_T 0 1 3 S→T_$ T→ 1_ 5 T→ 0 T 0_ • Cet automate sert à décrire toutes les formes sententielles ne contenant pas de handle. • Chaque état correspond à un item $ S→T$_ T→ 0 T_0 0 1 1 <accept> 4 0) S → T$ 1) T → 0 T 0 2) T → 1 • Seules formes sententielles conduisant à un item complet possèdent un handle. 43
Construction des tables à partir de l'automate ACTION 0 0 1 S 2 S 3 1 GOTO $ T 1 accept 2 S 3 3 R 2 4 S 5 5 R 1 44
Autre exemple • Ajoutons 3 règles à la grammaire précédente: (0) (1) (2) (3) (4) (5) S → T$ T → 0 T 0 T→ 1 T → 0 V V → 2 Note: Ceci n'est pas une grammaire LL mais c'est une grammaire LR 45
Automate fini non-déterministe 0 0 2 0 S→_T$ T→ 0_T 0 T 1 5 T→ 0 T 0_ T→ 0 T_0 3 7 T→ 1_ 0 V→ 2_ 2 $ 2 6 <accept> T→ 0_V S→T$_ 10 0 1 1 S→T_$ 4 T 8 2 V→ 2_V V 9 V→ 2 V_ V 2 T→ 0 V_ 46
Automate fini déterministe 0 4 2 S→_T$ T→ 0_T 0 1 S→T_$ • On peut toujours transformer un automate fini non-déterministe en un automate fini déterministe équivalent. • Chaque état correspond à un ensemble d'items. 1 1 3 T 0 T→ 1_ 0 5 T→ 0 T 0_ T→ 0 T_0 0 T 0 $ 2, 6 <accept> T→ 0_T 0 T→ 0_V S→T$_ 10 V 7, 8 2 V→ 2_V V 9 V→ 2 V_ T→ 0 V_ 2 47
Les conflits • Il existe deux types de conflits possibles lors de la construction des tables à partir de l'automate: 1. Shift-Reduce 1. Reduce-Reduce 48
Conflits Shift-Reduce • S→ 0 S 0|0 – Configuration initiale: (S 0, 000$) – Shift: (S 0 0 S 1, 00$) – Shift ou Reduce ? • Bison choisit toujours le Shift dans ce cas. • Remarque: Le mot ne sera donc pas accepté alors qu'il devrait l'être. 49
Conflits Reduce-Reduce • • 1. 2. 3. 4. S→ 0 S 0 S→T T→S 0 T→ 1 – – – – Configuration initiale: (S 0, 010$) Shift: (S 0 0 S 1, 10$) Shift: (S 0 0 S 1 1 S 2, 0$) Reduce: (S 0 0 S 1 T S 3, 0$) Reduce: (S 0 0 S 1 S S 3, 0$) Shift: (S 0 0 S 1 S S 3 0 S 4, $) Réduce: on utilise la règle 1 ou 3 ? Yacc choisit toujours la première des règles dans ce cas. Remarque: La grammaire n'est pas ambiguë. 50
Résumé • L'analyse syntaxique est une partie essentielle de L'implémentation d'un langage • Un analyseur lexical traduit une séquence de caractère (le programme source) en une séquence de tokens (et de lexèmes) • Un parseur récursif-descendant est un parseur LL implémenté en écrivant le code directement à partir de la grammaire du langage • Un parseur LR est l'approche la plus commune pour implémenter les analyseurs syntaxiques ascendants; c'est approche utilisé par Yacc. 51
- Slides: 50