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?
Réponses:
À 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)
Vous pouvez trivialement spécifier deux grammaires qui accepteront ces exemples:
Trivial Grammar One:
Trivial Grammar Two:
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
method
s dans une classe, vous voudrez une règle avec ce formulaire:Ce qui est mieux indiqué dans EBNF comme:
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
extends
clause 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
Person
l'â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.la source
Pour la plupart des générateurs d'analyseur, il est généralement une variante de Backus-Naur de
<nonterminal> ::= expression
format. 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.
"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:
Exemple d'entrée (tous valides):
la source
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
comme point de départ. Ou vous devrez peut-être l'écrire
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:C'est une notation ("syntaxe concrète") que vous pouvez choisir. Ou vous pouvez tout aussi facilement décider de cela:
ou
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 lesreturn
accolades ou si les deux branches d'uneif
dé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 comme1 + "a"
dans la grammaire, si vous avez travaillé assez dur, mais vous ne pouviez pas rejeter1 + x
(oùx
a la chaîne de type). Doncévitez les restrictions à moitié cuites dans la grammaire et faites-les correctement comme un passage séparé.la source