Quand est-ce que lexing suffit, quand avez-vous besoin d'EBNF?
EBNF n'ajoute vraiment pas beaucoup à la puissance des grammaires. C'est juste une commodité / notation de raccourci / "sucre syntaxique" sur les règles de grammaire standard de la forme normale (CNF) de Chomsky. Par exemple, l'alternative EBNF:
S --> A | B
vous pouvez réaliser dans CNF en listant simplement chaque production alternative séparément:
S --> A // `S` can be `A`,
S --> B // or it can be `B`.
L'élément optionnel d'EBNF:
S --> X?
vous pouvez réaliser dans CNF en utilisant une production nullable , c'est-à-dire celle qui peut être remplacée par une chaîne vide (dénotée par juste une production vide ici; d'autres utilisent epsilon ou lambda ou un cercle croisé):
S --> B // `S` can be `B`,
B --> X // and `B` can be just `X`,
B --> // or it can be empty.
Une production sous une forme comme la dernière ci- B
dessus est appelée "effacement", car elle peut effacer tout ce qu'elle représente dans d'autres productions (produire une chaîne vide au lieu de quelque chose d'autre).
Zéro ou plus de répétition de l'EBNF:
S --> A*
vous pouvez obtenir en utilisant production récursive , c'est-à-dire celle qui s'enfonce quelque part en elle. Cela peut se faire de deux manières. La première est la récursion gauche (ce qui devrait généralement être évité, car les analyseurs de descente récursive descendante ne peuvent pas l'analyser):
S --> S A // `S` is just itself ended with `A` (which can be done many times),
S --> // or it can begin with empty-string, which stops the recursion.
Sachant qu'il ne génère (finalement) qu'une chaîne vide suivie de zéro ou plus A
s, la même chaîne ( mais pas le même langage! ) Peut être exprimée en utilisant la récursion à droite :
S --> A S // `S` can be `A` followed by itself (which can be done many times),
S --> // or it can be just empty-string end, which stops the recursion.
Et quand il s'agit d' +
une ou plusieurs répétitions de l'EBNF:
S --> A+
cela peut être fait en factorisant un A
et en utilisant *
comme avant:
S --> A A*
que vous pouvez exprimer en CNF en tant que tel (j'utilise ici la bonne récursion; essayez de comprendre l'autre vous-même comme un exercice):
S --> A S // `S` can be one `A` followed by `S` (which stands for more `A`s),
S --> A // or it could be just one single `A`.
Sachant cela, vous pouvez maintenant probablement reconnaître une grammaire pour une expression régulière (c'est-à-dire, une grammaire régulière ) comme une grammaire qui peut être exprimée dans une seule production EBNF constituée uniquement de symboles terminaux. Plus généralement, vous pouvez reconnaître des grammaires régulières lorsque vous voyez des productions similaires à celles-ci:
A --> // Empty (nullable) production (AKA erasure).
B --> x // Single terminal symbol.
C --> y D // Simple state change from `C` to `D` when seeing input `y`.
E --> F z // Simple state change from `E` to `F` when seeing input `z`.
G --> G u // Left recursion.
H --> v H // Right recursion.
Autrement dit, en utilisant uniquement des chaînes vides, des symboles de terminal, des non-terminaux simples pour les substitutions et les changements d'état, et en utilisant la récursivité uniquement pour atteindre la répétition (itération, qui est juste une récursion linéaire - celle qui ne branche pas comme un arbre). Rien de plus avancé au-dessus de ceux-ci, alors vous êtes sûr que c'est une syntaxe régulière et vous pouvez aller avec juste lexer pour cela.
Mais lorsque votre syntaxe utilise la récursivité de manière non triviale, pour produire des structures imbriquées arborescentes, autosimilaires, comme la suivante:
S --> a S b // `S` can be itself "parenthesized" by `a` and `b` on both sides.
S --> // or it could be (ultimately) empty, which ends recursion.
alors vous pouvez facilement voir que cela ne peut pas être fait avec une expression régulière, car vous ne pouvez pas le résoudre en une seule production EBNF en aucune façon; vous finirez par remplacer S
indéfiniment, ce qui ajoutera toujours un autre a
s et un b
s des deux côtés. Les lexers (plus précisément: les automates à états finis utilisés par les lexeurs) ne peuvent pas compter comme des nombres arbitraires (ils sont finis, vous vous en souvenez?), Ils ne savent donc pas combien a
il y avait de correspondances égales avec autant de b
s. Des grammaires comme celle-ci sont appelées (au moins des grammaires sans contexte ), et elles nécessitent un analyseur.
Les grammaires sans contexte sont bien connues pour être analysées, elles sont donc largement utilisées pour décrire la syntaxe des langages de programmation. Mais il y a plus. Parfois, une grammaire plus générale est nécessaire - lorsque vous avez plus de choses à compter en même temps, indépendamment. Par exemple, lorsque vous voulez décrire une langue où l'on peut utiliser des parenthèses rondes et des accolades carrées entrelacées, mais elles doivent être appariées correctement les unes avec les autres (accolades avec accolades, rondes avec rondes). Ce type de grammaire est appelé sensible au contexte . Vous pouvez le reconnaître par le fait qu'il a plus d'un symbole sur la gauche (avant la flèche). Par exemple:
A R B --> A S B
Vous pouvez considérer ces symboles supplémentaires à gauche comme un "contexte" pour l'application de la règle. Il pourrait y avoir des conditions préalables, etc. postconditions Par exemple, la règle ci - dessus substituera R
en S
, mais seulement quand il est entre A
et B
, laissant ceux A
et B
se inchangés. Ce type de syntaxe est vraiment difficile à analyser, car il a besoin d'une machine de Turing à part entière. C'est une toute autre histoire, donc je vais terminer ici.