Les lexers et les analyseurs syntaxiques sont-ils vraiment si différents en théorie?
Il semble à la mode de détester les expressions régulières: coding horror , n autre article de blog .
Cependant, les outils populaires basés sur le lexing: pygments , geshi , ou prettify , utilisent tous des expressions régulières. Ils semblent rien Lex ...
Quand le lexing est-il suffisant, quand avez-vous besoin d'EBNF?
Quelqu'un a-t-il utilisé les jetons produits par ces lexers avec des générateurs d'analyse de bison ou d'antir?
Ce que les analyseurs et les lexers ont en commun:
*
, ==
, <=
, ^
seront classés comme jeton "opérateur" par le lexer C/C++.[number][operator][number]
, [id][operator][id]
, [id][operator][number][operator][number]
seront classées comme "expression" non finales par l'analyseur C/C++.[TXT][TAG][TAG][TXT][TAG][TXT]...
.Comme vous pouvez le constater, les analyseurs syntaxiques et les générateurs de jetons ont beaucoup en commun. Un analyseur peut être un jeton pour un autre analyseur, qui lit ses jetons d’entrée sous forme de symboles de son propre alphabet (les jetons sont simplement des symboles d’un alphabet) de la même manière que les phrases d’une langue peuvent être des symboles alphabétiques d’un autre, de niveau supérieur. la langue. Par exemple, si *
et -
sont les symboles de l'alphabet M
(en tant que "symboles de code Morse"), vous pouvez créer un analyseur qui reconnaît les chaînes de ces points et de ces lignes en tant que lettres codées en morse. Les phrases dans la langue "Code Morse" pourraient être des jetons pour un autre analyseur, pour lesquels ces jetons sont des symboles atomiques de sa langue (par exemple, la langue "Mots anglais"). Et ces "mots anglais" pourraient être des jetons (symboles de l'alphabet) d'un analyseur syntaxique de niveau supérieur qui comprend la langue des "phrases anglaises". Et toutes ces langues ne diffèrent que par la complexité de la grammaire. Rien de plus.
Alors, que dire de ces "niveaux de grammaire de Chomsky"? Eh bien, Noam Chomsky a classé les grammaires en quatre niveaux en fonction de leur complexité:
a
, b
), leurs concaténations (ab
, aba
, bbb
etd.), Ou des alternatives (par exemple a|b
).(()()(()()))
, balises HTML/BBcode imbriquées, blocs imbriqués, etc. C'est parce que les automates à états pour les traiter doivent comporter un nombre infini d'états pour gérer un nombre infini de niveaux d'imbrication.x+3
et que dans un contexte ceci x
pourrait être le nom d'une variable, et dans un autre contexte cela pourrait être le nom d'une fonction, etc.Oui, ils sont très différents en théorie et en implémentation.
Les lexers sont habitués à reconnaître les "mots" qui constituent des éléments de langage, car la structure de tels mots est généralement simple. Les expressions régulières sont extrêmement efficaces pour gérer cette structure simple, et il existe des moteurs de correspondance d'expression régulière très performants utilisés pour implémenter les lexeurs.
Les analyseurs syntaxiques sont utilisés pour reconnaître la "structure" d'une phrase. Une telle structure va généralement bien au-delà de ce que les "expressions régulières" peuvent reconnaître. Il faut donc utiliser des analyseurs syntaxiques "contextuels" pour extraire cette structure. Les analyseurs contextuels étant difficiles à créer, le compromis en matière d'ingénierie consiste à utiliser des grammaires "sans contexte" et à ajouter des hacks aux analyseurs syntaxiques ("tables de symboles", etc.) pour gérer la partie sensible au contexte.
Ni lexing ni la technologie d'analyse ne vont probablement disparaître bientôt.
Ils peuvent être unifiés en décidant d'utiliser la technologie "d'analyse syntaxique" pour reconnaître les "mots", comme l'expliquent actuellement les soi-disant analyseurs GLR sans scanner. Cela a un coût d'exécution, car vous appliquez des machines plus générales à ce qui est souvent un problème qui n'en a pas besoin et que vous payez généralement en frais généraux. Lorsque vous avez beaucoup de cycles gratuits, ces frais généraux ne sont peut-être pas importants. Si vous traitez beaucoup de texte, les frais généraux sont importants et les analyseurs syntaxiques d'expression classiques continueront d'être utilisés.
Quand le lexing est-il suffisant, quand avez-vous besoin d'EBNF?
EBNF n'ajoute pas grand chose à la puissance des grammaires. C'est juste une commodité/notation de raccourci/"sucre syntaxique" sur les règles de grammaire standard de Chomsky, la forme normale (CNF). Par exemple, l’alternative EBNF:
S --> A | B
vous pouvez obtenir en CNF en énumérant simplement chaque production alternative:
S --> A // `S` can be `A`,
S --> B // or it can be `B`.
L'élément optionnel de EBNF:
S --> X?
vous pouvez obtenir dans CNF en utilisant une production nullable, c'est-à-dire celle qui peut être remplacée par un chaîne vide (indiquée par une production simplement vide ici; d'autres utilisent epsilon ou lambda ou 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 B
ci-dessus est appelée "effacement", car elle peut effacer ce qu'elle représente dans d'autres productions (produit une chaîne vide au lieu de quelque chose d'autre).
Zéro ou plus répétition de EBNF:
S --> A*
vous pouvez obtenir en utilisant la production récursive, c'est-à-dire celle qui s'y intègre quelque part. Cela peut être fait de deux manières. Le premier est récursion gauche (ce qui devrait généralement être évité, car les analyseurs syntaxiques descendant récursifs 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’elle ne génère qu’une chaîne vide (au final) suivie de zéro ou plus de A
s, la même chaîne (mais pas le même langage!) peut être exprimée à l’aide de right- récursion:
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 de +
pour une ou plusieurs répétitions d’EBNF:
S --> A+
cela peut être fait en factorisant une A
et en utilisant *
comme auparavant:
S --> A A*
que vous pouvez exprimer en tant que tel dans CNF (j’utilise la bonne récursion ici; essayez de trouver l’autre comme 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 grammaire régulière) comme pouvant être exprimée dans une production EBNF unique composée uniquement de symboles terminaux. Plus généralement, vous pouvez reconnaître les 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.
C'est-à-dire que vous n'utilisez que des chaînes vides, des symboles de terminal, de simples non-terminaux pour les substitutions et les changements d'état, et que vous utilisez la récursivité uniquement pour obtenir une répétition (itération, qui est simplement récurrence linéaire - celle qui ne fonctionne branche semblable à un arbre). Rien de plus avancé au-dessus de ceux-ci, alors vous êtes sûr que c'est une syntaxe normale et vous pouvez utiliser simplement lexer pour cela.
Mais lorsque votre syntaxe utilise la récursivité de manière non triviale, vous créez des structures imbriquées semblables à des arbres, similaires à celles-ci, telles que 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, parce que vous ne pouvez pas le résoudre en une seule production EBNF de quelque manière que ce soit; vous finirez par substituer indéfiniment S
, ce qui ajoutera toujours un autre a
s et b
s des deux côtés. Les lexers (plus particulièrement: les automates à états finis utilisés par les lexers) ne peuvent compter en nombres arbitraires (ils sont finis, rappelez-vous?), Ils ne savent donc pas combien de a
s étaient là pour les faire correspondre de manière égale à tant de b
s. Les grammaires comme celui-ci s'appellent grammaires dépourvues de contexte (au minimum), et ils 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 souhaitez décrire une langue dans laquelle vous pouvez utiliser des parenthèses arrondies et des accolades carrées entrelacées, mais elles doivent être associées correctement les unes aux autres (accolades avec accolades, arrondies avec arrondies). Ce type de grammaire s'appelle sensible au contexte. Vous pouvez le reconnaître par le fait qu’il a plus d’un symbole à 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. Par exemple, la règle ci-dessus substituera R
à S
, mais seulement si c'est entre A
et B
, laissant ces A
et B
eux-mêmes inchangés. Ce type de syntaxe est vraiment difficile à analyser, car il nécessite une machine complète de Turing. C'est une toute autre histoire, alors je vais terminer ici.
Répondre à la question telle que posée (sans répéter indûment ce qui apparaît dans les autres réponses)
Lexers et parseurs ne sont pas très différents, comme le suggère la réponse acceptée. Les deux sont basés sur des formalismes de langage simples: des langages standard pour les lexers et, presque toujours, des langages sans contexte (CF) pour les analyseurs. Ils sont tous deux associés à des modèles de calcul relativement simples, à l'automate à états finis et à l'automate à pile Push-down. Les langues ordinaires sont un cas particulier de langues sans contexte, de sorte que des lexers puissent être produits avec la technologie CF un peu plus complexe. Mais ce n’est pas une bonne idée pour au moins deux raisons.
Un point fondamental de la programmation est qu’un composant du système doit être doté de la technologie la plus appropriée, de manière à ce qu’il soit facile à produire, à comprendre et à entretenir. La technologie ne doit pas être exagérée (utilisation de techniques beaucoup plus complexes et coûteuses que nécessaire), ni à la limite de sa puissance, ce qui nécessite des contorsions techniques pour atteindre l'objectif souhaité.
C'est pourquoi "il semble à la mode de détester les expressions régulières". Bien qu'ils puissent faire beaucoup, ils ont parfois besoin d'un code très illisible pour le réaliser, sans oublier le fait que diverses extensions et restrictions de mise en œuvre réduisent quelque peu leur simplicité théorique. Les Lexers ne le font généralement pas et constituent généralement une technologie simple, efficace et appropriée pour analyser les jetons. Utiliser des analyseurs syntaxiques des FC pour les jetons serait excessif, bien que ce soit possible.
Une autre raison de ne pas utiliser le formalisme des FC pour les lexistes est qu’il pourrait alors être tentant d’utiliser toute la puissance des FC. Mais cela pourrait poser des problèmes de structure en ce qui concerne la lecture des programmes.
Fondamentalement, la majeure partie de la structure du texte du programme, à partir de laquelle le sens est extrait, est une structure arborescente. Il exprime comment la phrase d'analyse (programme) est générée à partir de règles de syntaxe. La sémantique est dérivée par des techniques de composition (homomorphisme pour les orientés mathématiquement) de la manière dont les règles de syntaxe sont composées pour construire l'arbre d'analyse. Par conséquent, la structure arborescente est essentielle. Le fait que les jetons soient identifiés avec un lexer régulier basé sur un ensemble ne change pas la situation, car CF composé avec régulière donne toujours CF (je parle très vaguement des transducteurs réguliers, qui transforment un flux de caractères en un flux de jetons).
Cependant, la mucoviscidose composée avec CF (via des transducteurs CF ... désolé pour le calcul), ne donne pas nécessairement CF, et pourrait rendre les choses plus générales, mais moins faciles à gérer en pratique. Donc, CF n'est pas l'outil approprié pour les lexeurs, même s'il peut être utilisé.
L’une des différences majeures entre les langues classiques et les FC est que les langages normaux (et les transducteurs) se comportent très bien avec presque tous les formalismes de diverses manières, alors que les langages des FC (et les transducteurs) ne le font pas, même avec eux-mêmes (à quelques exceptions près).
(Notez que les transducteurs classiques peuvent avoir d'autres utilisations, telles que la formalisation de certaines techniques de traitement des erreurs de syntaxe.)
BNF n'est qu'une syntaxe spécifique pour présenter les grammaires des FC.
EBNF est un sucre syntaxique pour BNF , utilisant les fonctionnalités de la notation normale pour donner une version plus précise des grammaires BNF. Il peut toujours être transformé en une BNF pure équivalente.
Cependant, la notation régulière est souvent utilisée dans EBNF uniquement pour souligner ces parties de la syntaxe qui correspondent à la structure des éléments lexicaux et doivent être reconnues avec le lexer, le reste étant plutôt présenté en BNF simple. Mais ce n'est pas une règle absolue.
Pour résumer, la structure plus simple du jeton est mieux analysée avec la technologie plus simple des langages normaux, tandis que la structure arborescente du langage (de la syntaxe du programme) est mieux gérée par les grammaires CF
Je suggérerais également de regarder réponse de AHR .
Mais cela laisse une question ouverte: Pourquoi des arbres?
Les arbres sont une bonne base pour spécifier la syntaxe car
ils donnent une structure simple au texte
il est très pratique d'associer la sémantique au texte sur la base de cette structure, avec une technologie bien comprise sur le plan mathématique (compositionnalité via des homomorphismes), comme indiqué ci-dessus. C'est un outil algébrique fondamental pour définir la sémantique des formalismes mathématiques.
C'est donc une bonne représentation intermédiaire, comme en témoigne le succès des arbres de syntaxe abstraite (AST). Notez que AST sont souvent différents de l'arbre d'analyse car la technologie d'analyse utilisée par de nombreux professionnels (tels que LL ou LR) s'applique uniquement à un sous-ensemble de grammaires CF, ce qui entraîne des distorsions grammaticales qui sont corrigées ultérieurement dans AST. Ceci peut être évité avec une technologie d'analyse plus générale (basée sur une programmation dynamique) qui accepte toute grammaire CF.
Les déclarations sur le fait que les langages de programmation sont sensibles au contexte (CS) plutôt que CF sont arbitraires et discutables.
Le problème est que la séparation de la syntaxe et de la sémantique est arbitraire. Le contrôle des déclarations ou des accords de type peut être considéré comme faisant partie de la syntaxe ou de la sémantique. Il en irait de même de l’accord de genre et de nombre dans les langues naturelles. Mais il existe des langages naturels dans lesquels un accord pluriel dépend de la signification sémantique réelle des mots, de sorte qu'il ne correspond pas à la syntaxe.
De nombreuses définitions de langages de programmation en sémantique dénotationnelle placent des déclarations et des vérifications de type dans la sémantique. Donc, affirmant que cela est fait par Ira Baxter que les analyseurs syntaxiques des FC sont piratés pour obtenir la sensibilité au contexte requise par la syntaxe est au mieux une vue arbitraire de la situation. Cela peut être organisé comme un hack dans certains compilateurs, mais ce n’est pas nécessairement le cas.
De plus, il n’est pas seulement que les analyseurs syntaxiques CS (au sens utilisé dans d’autres réponses ici) sont difficiles à construire et moins efficaces. Ils sont également insuffisants pour exprimer clairement le type de sensibilité au contexte qui pourrait être nécessaire. Et ils ne produisent pas naturellement une structure syntaxique (telle que des arbres d’analyse) qui soit pratique pour dériver la sémantique du programme, c’est-à-dire pour générer le code compilé.
Il existe plusieurs raisons pour lesquelles la partie analyse d'un compilateur est normalement séparée en phases d'analyse lexicale et d'analyse (analyse syntaxique).
ressource ___Compilateurs (2e édition) écrit par- Alfred V. Abo Université Columbia Monica S. Lam Université Stanford Ravi Sethi Avaya Jeffrey D. Ullman Université Stanford