Composable Grammars

8

Es gibt so viele Programmiersprachen, die die Einbindung von Minisprachen unterstützen. PHP ist in HTML eingebettet. XML kann in JavaScript eingebettet werden. Linq kann in C # eingebettet werden. Reguläre Ausdrücke können in Perl eingebettet werden.

%Vor%

Wenn Sie darüber nachdenken, können die meisten Programmiersprachen als verschiedene Minisprachen modelliert werden. Java könnte zum Beispiel in mindestens vier verschiedene Minisprachen aufgeteilt werden:

  • Eine Typdeklarationssprache (Paketdirektive, Importierdirektiven, Klassendeklaration)
  • Eine Member-Deklarationssprache (Zugriffsmodifikatoren, Methodendeklarationen, Member-Vars)
  • Eine Anweisungssprache (Kontrollfluss, sequentielle Ausführung)
  • Eine Ausdruckssprache (Literale, Zuweisungen, Vergleiche, Arithmetik)

Die Fähigkeit, diese vier konzeptuellen Sprachen als vier verschiedene Grammatiken zu implementieren, würde sicherlich eine Menge des Spaghettiismus reduzieren, den ich normalerweise in komplexen Parser- und Compiler-Implementierungen sehe.

Ich habe bereits Parser für verschiedene Arten von Sprachen implementiert (mit ANTLR, JavaCC und benutzerdefinierten Recursive-Descent-Parsern), und wenn die Sprache wirklich groß und komplex wird, endet man normalerweise mit einer huuuuuuge Grammatik Parser-Implementierung wird sehr schnell wirklich hässlich.

Idealerweise wäre es beim Schreiben eines Parsers für eine dieser Sprachen sinnvoll, sie als eine Sammlung von zusammensetzbaren Parsern zu implementieren, wobei die Steuerung zwischen ihnen hin- und hergereicht wird.

Das Schwierige daran ist, dass die enthaltene Sprache (z. B. Perl) oft ihre eigene Terminalenkennung für die enthaltene Sprache (z. B. reguläre Ausdrücke) definiert. Hier ist ein gutes Beispiel:

%Vor%

In diesem Code definiert der Haupt-Perl-Code einen nicht standardmäßigen Terminus "|" für den regulären Ausdruck. Es wäre sehr schwierig, den Regex-Parser vollständig vom Perl-Parser abzugrenzen, da der Regex-Parser nicht wissen würde, wie er den Ausdruckstermin finden kann, ohne den Parser zu konsultieren.

Oder, sagen wir mal, ich hatte eine Sprache, die das Einfügen von Linq-Ausdrücken erlaubt, aber anstatt mit einem Semikolon zu enden (wie C #), wollte ich die Linq-Ausdrücke in eckigen Klammern angeben:

%Vor%

Wenn ich die Linq-Grammatik innerhalb der Grammatik der Elternsprache definiert habe, könnte ich leicht eine eindeutige Produktion für eine "LinqExpression" schreiben, indem ich syntaktische Lookahead verwendet, um die Klammergehäuse zu finden. Aber dann müsste meine Elterngrammatik die gesamte Linq-Spezifikation aufnehmen. Und das ist ein Widerstand. Auf der anderen Seite wäre es für einen separaten untergeordneten Linq-Parser sehr schwierig herauszufinden, wo er anhalten soll, da er Lookahead für fremde Token-Typen implementieren müsste.

Und das würde das Verwenden separater Lexing / Parsing-Phasen ziemlich ausschließen, da der Linq-Parser einen ganz anderen Satz von Tokenisierungsregeln definieren würde als der Parser. Wenn Sie nach einem Token nach dem anderen suchen, wissen Sie, wann Sie die Kontrolle an den lexikalischen Analysator der übergeordneten Sprache zurückgeben müssen.

Was denkt ihr? Was sind die besten heute verfügbaren Techniken zur Implementierung von unterschiedlichen, entkoppelten und zusammensetzbaren Sprachgrammatiken für die Einbeziehung von Minisprachen in größere Elternsprachen?

    
benjismith 04.06.2009, 21:23
quelle

6 Antworten

4

Vielleicht möchten Sie diesen Podcast anhören . Das scannerlose Parsen wurde "erfunden", um das Problem des Zusammensetzens verschiedener Grammatiken zu lösen (das Problem ist, dass Sie schnell feststellen, dass Sie keinen "universellen" Tokenizer / Scanner schreiben können).

    
Paul Hollingsworth 04.06.2009 22:46
quelle
3

Ich arbeite an genau diesem Problem. Ich werde meine Gedanken teilen:

Grammatiken sind schwer zu debuggen. Ich habe ein paar in Bison und ANTLR getestet und es war nicht schön. Wenn Sie möchten, dass der Benutzer DSLs als Grammatik in Ihren Parser einfügt, müssen Sie einen Weg finden, ihn so zu machen, dass er nicht explodiert. Mein Ansatz besteht darin, keine willkürlichen DSLs zuzulassen, sondern nur solche, die zwei Regeln folgen:

  • Die Tokentypen (Bezeichner, Zeichenfolgen, Nummern) sind zwischen allen DSLs in einer Datei gleich.
  • Unsymmetrische Klammern, geschweifte Klammern oder Klammern sind nicht zulässig

Der Grund für die erste Einschränkung liegt darin, dass moderne Parser das Parsen in eine lexikalische Stufe unterbrechen und dann Ihre traditionellen Grammatikregeln anwenden. Glücklicherweise glaube ich, dass ein einziger universeller Tokenizer für 90% der DSLs, die Sie erstellen möchten, gut genug ist, auch wenn er nicht die DSLs enthält, die Sie bereits erstellt haben und die Sie einbetten möchten.

Die zweite Einschränkung erlaubt es, die Grammatiken mehr voneinander zu trennen. Sie können in zwei Phasen analysieren, indem Sie Klammern (Klammern, Klammern) gruppieren und dann jede Gruppe rekursiv analysieren. Die Grammatik Ihres eingebetteten DSL kann nicht durch die Klammern entkommen, in denen es enthalten ist.

Ein weiterer Teil der Lösung besteht darin, Makros zuzulassen. Zum Beispiel, regex("abc*/[^.]") sieht gut aus für mich. Auf diese Weise kann das Makro " regex " die Regex analysieren, anstatt ein Regexgramm in die Hauptsprache zu schreiben. Sie können zwar nicht verschiedene Begrenzer für Ihre Regex verwenden, aber Sie erhalten ein gewisses Maß an Konsistenz in meinen Gedanken.

    
Dietrich Epp 04.06.2009 22:14
quelle
1

Das Parsen ist ein Aspekt des Problems, aber ich vermute, dass die Interoperation zwischen den verschiedenen ausführbaren Interpretern, die sich auf jede Minisprache beziehen, wahrscheinlich wesentlich schwieriger zu lösen ist. Um nützlich zu sein, muss jeder unabhängige Syntaxblock konsistent mit dem Gesamtkontext arbeiten (oder das endgültige Verhalten ist nicht vorhersagbar und daher unbrauchbar).

Nicht dass ich verstehe, was sie wirklich machen, aber ein sehr interessanter Ort, um nach mehr Inspiration zu suchen, ist FoNC . Sie scheinen (ich vermute) in eine Richtung zu steuern, in der alle Arten von verschiedenen Computer-Engines nahtlos interagieren können.

    
Paul W Homer 04.06.2009 21:50
quelle
1

Sehen Sie sich SGLR , Scannerloses generalisiertes LR-Parsing an. Hier sind einige Referenzen und URLs. Diese Analysetechniken machen die Zusammensetzung von Analysetabellen sehr einfach. Besonders in Kombination mit SDF .

Martin Bravenboer und Eelco Visser. Entwerfen von Syntaxeinbettungen und Assimilationen für Sprachbibliotheken. In Models in Software Engineering: Workshops und Symposien auf MoDELS 2007, Band 5002 von LNCS, 2008.

MetaBorg und MetaBorg in Aktion

    
Zef Hemel 09.06.2009 19:31
quelle
0

Wenn Sie darüber nachdenken, funktioniert das rekursive Descent-Parsing wirklich. Jede Regel und alle Regeln, von denen sie abhängt, bilden eine Mini-Grammatik. Etwas Höheres spielt keine Rolle. Sie könnten zum Beispiel eine Java-Grammatik mit ANTLR schreiben und alle verschiedenen "Minisprachen" in verschiedene Teile der Datei aufteilen.

Dies ist nicht sehr häufig, nur weil diese "Mini-Sprachen" oft viele Regeln teilen. Es wäre jedoch sicherlich schön, wenn Sie mit Tools wie ANTLR separate Grammatiken aus verschiedenen Dateien hinzufügen könnten. Dies würde es Ihnen ermöglichen, sie logisch zu trennen. Der Grund, warum dies wahrscheinlich nicht implementiert ist, ist wahrscheinlich, dass es sich um ein "kosmetisches" Problem handelt, das nur mit den Grammatikdateien selbst zusammenhängt und nicht selbst analysiert. Es wird auch nicht Ihren Code kürzer machen (obwohl es etwas leichter zu folgen ist). Das einzige technische Problem, das dies lösen würde, ist Namenskollisionen.

    
Zifre 04.06.2009 22:32
quelle
0

Perl 6 kann als eine Reihe von DSLs angesehen werden, die speziell zum Schreiben von Programmen entwickelt wurden.

Tatsächlich ist die Rakudo-Implementierung genau auf diese Weise aufgebaut.

Gerade Strings sind ein DSL mit Optionen, die Sie aktivieren oder deaktivieren können.

%Vor%

Es muss im Grunde aus zusammensetzbaren Grammatiken gemacht werden, damit dies funktioniert:

%Vor%

In Perl sind 6 Grammatiken nur eine Art von Klasse und Tokens sind eine Art von Methode.

%Vor% %Vor%

Beachten Sie, dass ich keine Aktionsklasse angezeigt habe, was praktisch ist, um den Parsebaum so zu transformieren, wie er erstellt wurde.

    
Brad Gilbert 22.01.2017 21:39
quelle