Comment dois-je spécifier une grammaire pour un analyseur?

12

Je programme depuis de nombreuses années, mais une tâche qui me prend encore trop de temps est de spécifier une grammaire pour un analyseur, et même après cet effort excessif, je ne suis jamais sûr que la grammaire que j'ai trouvée est bonne ( par toute mesure raisonnable de «bien»).

Je ne m'attends pas à ce qu'il existe un algorithme pour automatiser le processus de spécification d'une grammaire, mais j'espère qu'il existe des moyens de structurer le problème qui éliminent une grande partie des conjectures et des essais et erreurs de mon approche actuelle.

Ma première pensée a été de lire sur les analyseurs, et j'en ai fait une partie, mais tout ce que j'ai lu à ce sujet prend la grammaire comme une donnée (ou assez banale pour que l'on puisse la spécifier par inspection), et se concentre sur le problème de la traduction de cette grammaire en analyseur. Je suis intéressé par le problème immédiatement avant: comment spécifier la grammaire en premier lieu.

Je m'intéresse principalement au problème de la spécification d'une grammaire qui représente formellement une collection d'exemples concrets (positifs et négatifs). Ceci est différent du problème de la conception d'une nouvelle syntaxe . Merci à Macneil d'avoir souligné cette distinction.

Je n'avais jamais vraiment apprécié la distinction entre une grammaire et une syntaxe, mais maintenant que je commence à la voir, je pourrais affiner ma première clarification en disant que je suis principalement intéressé par le problème de la spécification d'une grammaire qui imposera une syntaxe prédéfinie: il se trouve que dans mon cas, la base de cette syntaxe est généralement une collection d'exemples positifs et négatifs.

Comment la grammaire est-elle spécifiée pour un analyseur? Existe-t-il un livre ou une référence qui est la norme de facto pour décrire les meilleures pratiques, les méthodologies de conception et d'autres informations utiles sur la spécification d'une grammaire pour un analyseur? Sur quels points, en lisant la grammaire de l'analyseur, dois-je me concentrer?

anon
la source
1
J'ai modifié un peu votre question pour vous concentrer sur votre problème réel. Ce site est exactement le genre d'endroit où vous pouvez poser vos questions sur les grammaires et les analyseurs et obtenir des réponses d'experts. S'il existe des ressources externes qui méritent d'être examinées, elles apparaîtront naturellement dans les réponses qui vous aident directement.
Adam Lear
8
@kjo C'est malheureux. Si tout ce que vous demandez est une liste de références, vous n'utilisez pas Stack Exchange à son plein potentiel. Votre méta-problème n'utilise pas le site comme prévu. Les questions de liste sont presque universellement déconseillées sur Stack Exchange car elles ne s'intègrent pas très bien dans le modèle Q&R. Je vous recommande fortement de changer votre état d'esprit en posant des questions qui ont des réponses, pas des éléments, des idées ou des opinions .
Adam Lear
3
@kjo C'est certainement une question, mais pas la bonne question à poser sur Stack Exchange . SE n'est pas là pour construire des listes de références. C'est ici pour être la référence. Veuillez lire la méta-publication à laquelle j'ai lié dans mon commentaire pour une explication plus détaillée.
Adam Lear
5
@kjo: S'il vous plaît, ne vous découragez pas! Les modifications d'Anna ont gardé le cœur et le cœur de votre question et elle vous a aidé en rendant votre question plus conforme à la forme attendue sur Programmers.SE. Je ne connais aucune référence définitive que vous recherchez, mais j'ai pu y répondre. [OTOH, si j'avais connu une telle référence, je l'aurais certainement incluse.] Nous voulons encourager plus de réponses comme la mienne parce que, dans ce cas spécifique, je ne crois pas qu'il existe une référence pour ce que vous cherchez, juste expérience de parler aux autres.
Macneil
4
@kjo Je suis revenu aux modifications d'Anna et j'ai essayé d'incorporer un appel spécifique à une référence canonique sur la base de nos conseils pour les recommandations de livres : il y a beaucoup de bonnes informations dans les réponses fournies, et de les invalider en faisant la portée de la la seule question de trouver un livre serait un gaspillage. Maintenant, si nous pouvons tous arrêter la guerre de modification, ce serait génial.

Réponses:

12

À partir des fichiers d'exemple, vous devrez prendre des décisions en fonction de la quantité que vous souhaitez généraliser à partir de ces exemples. Supposons que vous disposiez des trois exemples suivants: (chacun est un fichier distinct)

f() {}
f(a,b) {b+a}
int x = 5;

Vous pouvez trivialement spécifier deux grammaires qui accepteront ces exemples:

Trivial Grammar One:

start ::= f() {} | f(a,b) {b+a} | int x = 5;

Trivial Grammar Two:

start ::= tokens
tokens ::= token tokens | <empty>
token ::= identifier | literal | { | } | ( | ) | , | + | = | ;

Le premier est trivial car il n'accepte que les trois échantillons. Le second est trivial car il accepte tout ce qui pourrait éventuellement utiliser ces types de jetons. [Pour cette discussion, je vais supposer que vous n'êtes pas très préoccupé par la conception du tokenizer: il est simple de supposer que les identifiants, les nombres et la ponctuation sont vos jetons, et vous pouvez emprunter n'importe quel ensemble de jetons à partir de n'importe quel langage de script que vous auriez comme de toute façon.]

Ainsi, la procédure que vous devrez suivre est de commencer au niveau supérieur et de décider "combien de chaque instance dois-je autoriser?" Si une construction syntaxique peut avoir du sens à répéter un certain nombre de fois, comme methods dans une classe, vous voudrez une règle avec ce formulaire:

methods ::= method methods | empty

Ce qui est mieux indiqué dans EBNF comme:

methods ::= {method}

Cela sera probablement évident lorsque vous ne voulez que zéro ou une instance (ce qui signifie que la construction est facultative, comme avec la extendsclause pour une classe Java), ou lorsque vous voulez autoriser une ou plusieurs instances (comme avec un initialiseur de variable dans une déclaration ). Vous devrez être attentif aux problèmes tels que l'exigence d'un séparateur entre les éléments (comme dans la ,liste d'arguments), l'exigence d'un terminateur après chaque élément (comme pour les ;instructions à séparer), ou l'exigence d'aucun séparateur ou terminateur (comme le cas avec des méthodes dans une classe).

Si votre langue utilise des expressions arithmétiques, il vous serait facile de copier à partir des règles de priorité d'une langue existante. Il vaut mieux s'en tenir à quelque chose de bien connu, comme les règles d'expressions de C, que d'aller à quelque chose d'exotique, mais à condition que tout le reste soit égal.

En plus des problèmes de priorité (ce qui est analysé) et des problèmes de répétition (combien de chaque élément devrait se produire, comment sont-ils séparés?), Vous devrez également penser à l'ordre: quelque chose doit-il toujours apparaître avant une autre chose? Si une chose est incluse, une autre devrait-elle être exclue?

À ce stade, vous pouvez être tenté d'appliquer grammaticalement certaines règles, une règle telle que si Personl'âge d'un enfant est spécifié, vous ne voulez pas que sa date de naissance soit également spécifiée. Bien que vous puissiez construire votre grammaire pour le faire, vous trouverez peut-être plus facile d'appliquer cela avec une passe de "vérification sémantique" après que tout soit analysé. Cela simplifie la grammaire et, à mon avis, améliore les messages d'erreur lorsque la règle est violée.

Macneil
la source
1
+1 pour de meilleurs messages d'erreur. La plupart des utilisateurs de votre langue ne seront pas des experts, qu'ils soient 10 ou 10 millions. La théorie de l'analyse a négligé cet aspect depuis trop longtemps.
MSalters
10

Où puis-je apprendre à spécifier la grammaire d'un analyseur?

Pour la plupart des générateurs d'analyseur, il est généralement une variante de Backus-Naur de <nonterminal> ::= expressionformat. Je vais partir du principe que vous utilisez quelque chose comme ça et que vous n'essayez pas de construire vos analyseurs à la main. Si vous pouvez produire un analyseur pour un format dans lequel vous avez reçu la syntaxe (j'ai inclus un exemple de problème ci-dessous), la spécification des grammaires n'est pas votre problème.

Ce que je pense que vous êtes contre, c'est la syntaxe de divination à partir d'un ensemble d'échantillons, ce qui est vraiment plus la reconnaissance des motifs que l'analyse. Si vous devez y recourir, cela signifie que la personne qui fournit vos données ne peut pas vous donner sa syntaxe, car elle ne maîtrise pas son format. Si vous avez la possibilité de repousser et de leur dire de vous donner une définition formelle, faites-le. Il n'est pas juste pour eux de vous donner un vague problème si vous pourriez être tenu responsable des conséquences d'un analyseur basé sur une syntaxe déduite acceptant une mauvaise entrée ou rejetant une bonne entrée.

... Je ne suis jamais sûr que la grammaire que j'ai trouvée est bonne (selon toute mesure raisonnable de "bonne").

"Bon" dans votre situation devrait signifier "analyse les points positifs et rejette les points négatifs". Sans aucune autre spécification formelle de la syntaxe de votre fichier d'entrée, les échantillons sont vos seuls cas de test et vous ne pouvez pas faire mieux que cela. Vous pourriez mettre le pied à terre et dire que seuls les exemples positifs sont bons et rejeter quoi que ce soit d'autre, mais ce n'est probablement pas dans l'esprit de ce que vous essayez d'accomplir.

Dans des circonstances plus saines, tester une grammaire est comme tester n'importe quoi d'autre: vous devez trouver suffisamment de cas de test pour exercer toutes les variantes des non-terminaux (et des terminaux, s'ils sont générés par un lexer).


Exemple de problème

Écrivez une grammaire qui analysera les fichiers texte contenant une liste telle que définie par les règles ci-dessous:

  • Une liste se compose de zéro ou plusieurs éléments .
  • Une chose se compose d'un identifiant , d'une accolade ouverte, d'une liste d'articles et d'une accolade fermante.
  • Une _item_list_ se compose de zéro ou plusieurs éléments .
  • Un élément composé d'un identifiant , d'un signe égal, d'un autre identifiant et d'un point-virgule.
  • Un identifiant est une séquence d'un ou plusieurs des caractères AZ, az, 0-9 ou le trait de soulignement.
  • L'espace est ignoré.

Exemple d'entrée (tous valides):

clank { foo = bar; baz = bear; }
clunk {
    quux =bletch;
    281_apple = OU812;
    He_Eats=Asparagus ; }
Blrfl
la source
2
Et assurez-vous d'utiliser "une variante de Backus-Naur" et non BNF lui-même. BNF peut exprimer une grammaire, mais cela rend beaucoup de concepts très courants, tels que les listes, beaucoup plus compliqués qu'ils ne devraient l'être. Il existe différentes versions améliorées, telles que EBNF, qui améliorent ces problèmes.
Mason Wheeler
7

Les réponses de Macneil et Blrfl sont excellentes. Je veux juste ajouter quelques commentaires sur le début du processus.

Une syntaxe n'est qu'un moyen de représenter un programme . La syntaxe de votre langue doit donc être déterminée par votre réponse à cette question: qu'est-ce qu'un programme?

Vous pourriez dire qu'un programme est une collection de classes. D'accord, cela nous donne

program ::= class*

comme point de départ. Ou vous devrez peut-être l'écrire

program ::= ε
         |  class program

Maintenant, qu'est-ce qu'une classe? Il a un nom; une spécification de superclasse facultative; et un tas de déclarations de constructeur, de méthode et de champ. En outre, vous avez besoin d'un moyen de regrouper une classe en une seule unité (sans ambiguïté), et cela devrait impliquer quelques concessions à l'utilisation (par exemple, étiquetez-la avec le mot réservé class). D'accord:

class ::= "class" identifier extends-clause? "{" class-member-decl * "}"

C'est une notation ("syntaxe concrète") que vous pouvez choisir. Ou vous pouvez tout aussi facilement décider de cela:

class ::= "(" "class" identifier extends-clause "(" class-member-decl* ")" ")"

ou

class ::= "class" identifier "=" "CLASS" extends-clause? class-member-decl* "END"

Vous avez probablement déjà pris cette décision implicitement, surtout si vous avez des exemples, mais je veux juste renforcer le point: la structure de la syntaxe est déterminée par la structure des programmes qu'elle représente. C'est ce qui vous fait dépasser les "grammaires triviales" de la réponse de Macneil. Cependant, les exemples de programmes sont toujours très importants. Ils servent à deux fins. Tout d'abord, ils vous aident à comprendre, au niveau abstrait, ce qu'est un programme. Deuxièmement, ils vous aident à décider quelle syntaxe concrète vous devez utiliser pour représenter la structure de votre langage.

Une fois que vous avez la structure en place, vous devez revenir en arrière et traiter des problèmes tels que l'autorisation des espaces et des commentaires, la correction des ambiguïtés, etc. la technologie d'analyse que vous utilisez.

Enfin, n'essayez pas de tout représenter sur votre langue dans la grammaire. Par exemple, vous pouvez vouloir interdire certains types de code inaccessible (par exemple, une instruction après a return, comme en Java). Vous ne devriez probablement pas essayer de mettre cela dans la grammaire, car vous allez manquer des choses (oups, que faire si les returnaccolades ou si les deux branches d'une ifdéclaration reviennent?) Ou vous rendrez votre grammaire trop compliquée gérer. C'est une contrainte contextuelle ; l'écrire comme un passage séparé. Un autre exemple très courant de contrainte contextuelle est un système de types. Vous pourriez rejeter des expressions comme 1 + "a"dans la grammaire, si vous avez travaillé assez dur, mais vous ne pouviez pas rejeter 1 + x(où xa la chaîne de type). Doncévitez les restrictions à moitié cuites dans la grammaire et faites-les correctement comme un passage séparé.

Ryan Culpepper
la source