Wie soll ich eine Grammatik für einen Parser angeben?

12

Ich programmiere schon seit vielen Jahren, aber eine Aufgabe, die mich immer noch überfordert, ist die Angabe einer Grammatik für einen Parser, und selbst nach dieser übermäßigen Anstrengung bin ich mir nie sicher, ob die Grammatik, die ich mir ausgedacht habe, gut ist ( durch ein vernünftiges Maß an "gut").

Ich erwarte nicht, dass es einen Algorithmus zur Automatisierung des Vorgangs zum Festlegen einer Grammatik gibt, aber ich hoffe, dass es Möglichkeiten gibt, das Problem so zu strukturieren, dass das Rätselraten und das Ausprobieren meines aktuellen Ansatzes weitgehend beseitigt werden.

Mein erster Gedanke war, über Parser zu lesen, und ich habe einige davon gemacht, aber alles, was ich zu diesem Thema gelesen habe, nimmt die Grammatik als gegeben (oder trivial genug, dass man sie durch Inspektion spezifizieren kann) und konzentriert sich auf das Problem der Übersetzung dieser Grammatik in einen Parser. Ich interessiere mich gleich vorher für das Problem: wie man die Grammatik überhaupt spezifiziert.

Ich interessiere mich hauptsächlich für das Problem, eine Grammatik anzugeben , die formal eine Sammlung konkreter Beispiele (positiv und negativ) darstellt. Dies unterscheidet sich vom Problem beim Entwerfen einer neuen Syntax . Vielen Dank an Macneil, der auf diesen Unterschied hingewiesen hat.

Ich hatte den Unterschied zwischen einer Grammatik und einer Syntax nie wirklich geschätzt, aber jetzt, wo ich anfange, dies zu sehen, konnte ich meine erste Klarstellung schärfen, indem ich sagte, dass ich hauptsächlich an dem Problem interessiert bin, eine Grammatik anzugeben, die a erzwingt Vordefinierte Syntax: In meinem Fall ist die Basis für diese Syntax normalerweise eine Sammlung von positiven und negativen Beispielen.

Wie wird die Grammatik für einen Parser angegeben? Gibt es ein Buch oder eine Referenz, die de facto als Standard für die Beschreibung von Best Practices, Entwurfsmethoden und anderen hilfreichen Informationen zur Angabe einer Grammatik für einen Parser gilt? Auf welche Punkte sollte ich mich konzentrieren, wenn ich über Parser-Grammatik lese?

anon
quelle
1
Ich habe Ihre Frage ein wenig bearbeitet, um mich auf Ihr eigentliches Problem zu konzentrieren. Auf dieser Website können Sie genau die Fragen zu Grammatiken und Parsern stellen und Antworten von Experten erhalten. Wenn es externe Ressourcen gibt, die einen Blick wert sind, tauchen sie natürlich in den Antworten auf, die Ihnen direkt helfen.
Adam Lear
8
@kjo Das ist bedauerlich. Wenn Sie nur eine Liste mit Referenzen benötigen, können Sie Stack Exchange nicht optimal nutzen. Ihr Meta-Problem verwendet die Site nicht wie beabsichtigt. Von Listenfragen wird in Stack Exchange generell abgeraten, da sie nicht sehr gut in das Q & A-Modell passen. Ich empfehle Ihnen nachdrücklich, Ihre Denkweise dahingehend zu ändern , dass Sie Fragen stellen, die Antworten enthalten, keine Elemente, Ideen oder Meinungen .
Adam Lear
3
@kjo Es ist sicherlich eine Frage, aber nicht die richtige Frage, die man bei Stack Exchange stellen sollte . SE ist nicht hier, um Referenzlisten zu erstellen. Es ist hier, um die Referenz zu sein. Bitte lesen Sie den Meta-Post, auf den ich in meinem Kommentar verlinkt habe, für eine detailliertere Erklärung.
Adam Lear
5
@kjo: Bitte lass dich nicht entmutigen! Annas Bearbeitungen haben den Kern und das Herz Ihrer Frage bewahrt, und sie hat Ihnen geholfen, indem sie Ihre Frage mehr in die Form gebracht hat, die wir von Programmers.SE erwarten. Ich kenne keine endgültigen Referenzen, die Sie suchen, konnte aber eine Antwort geben. [OTOH, hätte ich eine solche Referenz gekannt, hätte ich sie mit Sicherheit aufgenommen.] Wir möchten mehr Antworten wie meine ermutigen, weil ich in diesem speziellen Fall nicht glaube, dass es eine Referenz für das gibt, wonach Sie suchen, nur Erfahrung aus dem Gespräch mit anderen.
Macneil
4
@kjo Ich bin zu Annas Änderungen zurückgekehrt und habe versucht, einen bestimmten Aufruf für eine kanonische Referenz auf der Grundlage unserer Anleitung für Buchempfehlungen zu integrieren : Die Antworten enthalten viele gute Informationen und machen sie ungültig, indem sie in den Geltungsbereich der aufgenommen werden Die Frage, nur ein Buch zu finden, wäre eine Verschwendung. Wenn wir jetzt alle mit dem Edit Warring aufhören können, wäre das großartig.

Antworten:

12

Aus den Beispieldateien müssen Sie Entscheidungen treffen, die darauf basieren, wie stark Sie diese Beispiele verallgemeinern möchten. Angenommen, Sie hatten die folgenden drei Beispiele: (jedes ist eine separate Datei)

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

Sie könnten einfach zwei Grammatiken angeben, die diese Beispiele akzeptieren:

Trivial Grammatik Eins:

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

Triviale Grammatik Zwei:

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

Der erste ist trivial, weil er nur die drei Samples akzeptiert . Der zweite ist trivial, weil er alles akzeptiert, was möglicherweise diese Tokentypen verwenden könnte. [Für diese Diskussion gehe ich davon aus, dass Sie sich keine großen Sorgen um das Tokenizer-Design machen: Es ist einfach, Bezeichner, Zahlen und Interpunktionszeichen als Ihre Token anzunehmen, und Sie können jedes Token-Set aus jeder Skriptsprache ausleihen, die Sie möchten wie auch immer.]

Das Verfahren, das Sie befolgen müssen, besteht darin, auf der höchsten Ebene zu beginnen und zu entscheiden, wie viele Instanzen ich zulassen möchte. Wenn es sinnvoll sein kann, ein syntaktisches Konstrukt beliebig oft zu wiederholen, z. B. methods in einer Klasse, möchten Sie eine Regel mit der folgenden Form:

methods ::= method methods | empty

Was in EBNF besser ausgedrückt wird als:

methods ::= {method}

Es ist wahrscheinlich offensichtlich, wenn Sie nur null oder eine Instanz möchten (was bedeutet, dass das Konstrukt optional ist, wie bei der extendsKlausel für eine Java-Klasse), oder wenn Sie eine oder mehrere Instanzen zulassen möchten (wie bei einem Variableninitialisierer in einer Deklaration) ). Sie müssen Probleme berücksichtigen, wie das Erfordernis eines Trennzeichens zwischen Elementen (wie ,in einer Argumentliste), das Erfordernis eines Abschlusszeichens nach jedem Element (wie in den ;zu trennenden Anweisungen) oder das Erfordernis eines Trennzeichens oder Abschlusszeichens (wie im Fall) mit Methoden in einer Klasse).

Wenn Ihre Sprache arithmetische Ausdrücke verwendet, ist es für Sie einfach, die Vorrangregeln einer vorhandenen Sprache zu kopieren. Es ist am besten, sich an etwas Bekanntes zu halten, wie z. B. Cs Ausdrücke, als sich für etwas Exotisches zu entscheiden, aber nur, wenn alles andere gleich ist.

Neben Prioritätsproblemen (was wird miteinander analysiert) und Wiederholungsproblemen (wie viele Elemente sollten auftreten, wie werden sie getrennt?) Müssen Sie auch über die Reihenfolge nachdenken: Muss immer etwas vor einem anderen Element stehen? Wenn eine Sache enthalten ist, sollte eine andere ausgeschlossen werden?

An diesem Punkt könnten Sie versucht sein, einige Regeln grammatikalisch durchzusetzen. Wenn beispielsweise Persondas Alter einer Person angegeben ist, möchten Sie nicht, dass auch das Geburtsdatum angegeben wird. Sie können zwar Ihre Grammatik dafür konstruieren, es ist jedoch möglicherweise einfacher, dies mit einem "semantischen Überprüfungsdurchlauf" zu erzwingen, nachdem alles analysiert wurde. Dies vereinfacht die Grammatik und sorgt meiner Meinung nach für bessere Fehlermeldungen bei Regelverstößen.

Macneil
quelle
1
+1 für bessere Fehlermeldungen. Die meisten Benutzer Ihrer Sprache sind keine Experten, egal ob 10 oder 10 Millionen. Die Parsing-Theorie hat diesen Aspekt viel zu lange vernachlässigt.
MSalters
10

Wo kann ich lernen, wie man die Grammatik für einen Parser angibt?

Für die meisten Parser-Generatoren ist dies normalerweise eine Variante des Backus-Naur - <nonterminal> ::= expressionFormats. Ich gehe davon aus, dass Sie so etwas verwenden und nicht versuchen, Ihre Parser von Hand zu erstellen. Wenn Sie einen Parser für ein Format erstellen können, für das Sie die Syntax angegeben haben (ich habe unten ein Beispielproblem aufgeführt), ist die Angabe von Grammatiken nicht Ihr Problem.

Ich denke, Sie haben es mit Syntax zu tun, die aus einer Reihe von Samples abgeleitet wird. Das ist wirklich mehr Mustererkennung als Syntaxanalyse. Wenn Sie darauf zurückgreifen müssen, bedeutet dies, dass derjenige, der Ihre Daten bereitstellt, Ihnen die Syntax nicht mitteilen kann, da er das Format nicht gut beherrscht. Wenn Sie die Möglichkeit haben, zurückzudrängen und ihnen eine formale Definition zu geben, tun Sie dies. Es ist nicht fair für sie, Ihnen ein vages Problem zu geben, wenn Sie für die Folgen eines Parsers verantwortlich gemacht werden könnten, der auf einer abgeleiteten Syntax basiert, die falsche Eingaben akzeptiert oder gute Eingaben ablehnt.

... Ich bin mir nie sicher, ob die Grammatik, auf die ich gekommen bin, gut ist (mit einem vernünftigen Maß an "gut").

"Gut" in Ihrer Situation müsste bedeuten "analysiert die positiven und lehnt die negativen ab." Ohne eine andere formale Spezifikation der Syntax Ihrer Eingabedatei sind die Beispiele Ihre einzigen Testfälle, und Sie können nichts Besseres tun. Sie könnten Ihren Fuß nach unten setzen und sagen, dass nur die positiven Beispiele gut sind und alles andere ablehnen, aber das entspricht wahrscheinlich nicht dem Geist dessen, was Sie erreichen wollen.

Unter vernünftigen Umständen ist das Testen einer Grammatik wie das Testen von allem anderen: Sie müssen sich genügend Testfälle einfallen lassen, um alle Varianten der Nichtterminals (und Terminals, wenn sie von einem Lexer generiert werden) zu testen.


Beispielproblem

Schreiben Sie eine Grammatik, die Textdateien analysiert, die eine Liste enthalten, wie in den folgenden Regeln definiert:

  • Eine Liste besteht aus null oder mehr Dingen .
  • Ein Ding besteht aus einer Kennung , einer offenen Klammer, eine Artikelliste und einer schließenden Klammer.
  • Eine _item_list_ besteht aus null oder mehr Elementen .
  • Ein Artikel constsis eine Kennung , ein Gleichheitszeichen, eine andere Kennung und ein Semikolon.
  • Ein Bezeichner ist eine Folge von einem oder mehreren der Zeichen AZ, az, 0-9 oder dem Unterstrich.
  • Leerzeichen werden ignoriert.

Eingabebeispiel (alle gültig):

clank { foo = bar; baz = bear; }
clunk {
    quux =bletch;
    281_apple = OU812;
    He_Eats=Asparagus ; }
Blrfl
quelle
2
Und stellen Sie sicher, dass Sie "eine Variante von Backus-Naur" und nicht BNF selbst verwenden. BNF kann eine Grammatik ausdrücken, aber es macht viele sehr gebräuchliche Begriffe wie Listen viel komplizierter, als sie sein müssen. Es gibt verschiedene verbesserte Versionen wie EBNF, die diese Probleme beheben.
Mason Wheeler
7

Die Antworten von Macneil und Blrfl sind großartig. Ich möchte nur einige Kommentare zum Beginn des Prozesses hinzufügen.

Eine Syntax ist nur eine Möglichkeit, ein Programm darzustellen . Die Syntax Ihrer Sprache sollte also durch Ihre Antwort auf diese Frage bestimmt werden: Was ist ein Programm?

Man könnte sagen, dass ein Programm eine Sammlung von Klassen ist. Okay, das gibt uns

program ::= class*

als Ausgangspunkt. Oder du musst es vielleicht schreiben

program ::= ε
         |  class program

Was ist nun eine Klasse? Es hat einen Namen; eine optionale Oberklassenspezifikation; und eine Reihe von Konstruktor-, Methoden- und Felddeklarationen. Außerdem benötigen Sie eine Möglichkeit, eine Klasse in eine einzelne (eindeutige) Einheit zu gruppieren, und dies sollte einige Zugeständnisse an die Benutzerfreundlichkeit mit sich bringen (z. B. Kennzeichnen mit dem reservierten Wort class). Okay:

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

Das ist eine Notation ("konkrete Syntax"), die Sie wählen können. Oder Sie können sich genauso gut dafür entscheiden:

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

oder

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

Sie haben diese Entscheidung wahrscheinlich bereits implizit getroffen, insbesondere wenn Sie Beispiele haben, aber ich möchte nur den Punkt verstärken: Die Struktur der Syntax wird durch die Struktur der Programme bestimmt, die sie darstellen. Das ist es, was Sie über die "trivialen Grammatiken" von Macneils Antwort hinausführt. Beispielprogramme sind jedoch nach wie vor sehr wichtig. Sie dienen zwei Zwecken. Erstens helfen sie Ihnen dabei, auf abstrakter Ebene herauszufinden, was ein Programm ist. Zweitens helfen sie Ihnen bei der Entscheidung, welche konkrete Syntax Sie verwenden sollten, um die Struktur Ihrer Sprache darzustellen.

Sobald Sie die Struktur festgelegt haben, sollten Sie sich erneut mit Fragen wie dem Zulassen von Leerzeichen und Kommentaren, dem Beheben von Mehrdeutigkeiten usw. befassen Analyse-Technologie, die Sie verwenden.

Versuchen Sie schließlich nicht, alles über Ihre Sprache in der Grammatik darzustellen. Beispielsweise möchten Sie möglicherweise bestimmte Arten von nicht erreichbarem Code verbieten (z. B. eine Anweisung nach einem return, wie in Java). Sie sollten wahrscheinlich nicht versuchen, das in die Grammatik zu stopfen, weil Sie entweder Dinge verpassen (whoops, was returnist , wenn in geschweiften Klammern oder wenn beide Zweige einer ifAnweisung zurückkehren?) Oder Ihre Grammatik viel zu kompliziert machen managen. Es ist eine kontextsensitive Einschränkung. schreibe es als separaten Durchgang. Ein weiteres sehr verbreitetes Beispiel für eine kontextsensitive Einschränkung ist ein Typensystem. Sie könnten Ausdrücke wie 1 + "a"in der Grammatik ablehnen , wenn Sie hart genug gearbeitet haben, aber Sie könnten nicht ablehnen 1 + x(wo xhat Typ Zeichenfolge). SoVermeiden Sie halbherzige Einschränkungen in der Grammatik und machen Sie sie korrekt als separaten Durchgang.

Ryan Culpepper
quelle