date-created: 2024-05-14 12:21:37 date-modified: 2024-05-19 12:24:50

Parser | und das als DFA!

anchored to 112.00_anchor_overview


Overview

We would like to evaluate and understand how code can be highlighted given the a certain language, how its possible to parse a string denoting a command with flags etc to actually execute it correctly.

For that we are going to explore the world of parsers.

Parser:

Parser sind Programme, die einen String in eine Datenstruktur umwandeln können.

Dabei sind sie der erste Schritt vom Computerprogramm, wie wir sie so schon kennen, hin zum ausführbaren Programm. Umgesetzt wird das durch die Produktion eines Ableitungsbaum der Eingabe, durch einen Compiler / Interpreter –> wir wollen also verschiedene Beispiele der Grammatik anwenden, um so zu schauen, wie sie translatieren werden kann.

Motivation |

Wir wollen konzeptuell einen Parser bauen, welcher aufgrund von Regeln einer Grammatik eine Sprache erkennen bzw einen Inhalt eines Strings auf die Sprache prüft. Anders beschrieben also: -> Ein parser rekonstruiert die Ableitung eines Strings in einer kontextfreien Grammatik aus dem String selbst. Das kann man auch als einen Graphen für den String verstehen.

[!Tip] Eindeutigkeit der Ableitung Dabei kann hier schon erkannt werden, dass diese Ableitung des Strings nicht zwingend eindeutig sein muss / kann. Es können also lokale Matching gemacht werden, die eindeutig erscheinen, aber unter größerem Kontext dann vielleicht nicht richtig oder nicht eindeutig sind. –> etwa wenn unsere Grammatik sagt, dass sie “aaabb” sieht und daraus dann entweder:

  • aa, abb oder
  • aaa,bb erkennt. Wir müssten dann schauen, wir wir hiermit eine eindeutigkeit schaffen, weil diese Erkennung ja zuerst lokal passiert und wir den globalen Kontext noch nicht kennen können

Das ganze kann man jetzt noch an einem Beispiel festmachen, bzw zeigen.

naives parsing einer Sprache

Wir möchten einen naiven und simplen Ansatz zum parsen einer Grammatik bzw zum parsen eines Strings zum Prüfen, ob er von der Grammatik abgedeckt wird, betrachten. Betrachten wir hierbei eine CFG - Grammatik ( context free). und ferner betrachten wir das Wort . Ein Ansatz:

Wir könnten naiv einfach alle Regeln ausprobieren und schauen, ob unsere Sprache das Wort erkennen kann. Dabei nehmen wir die Regeln, die nicht passen auch nicht auf.

Am Beispiel schaut man sich jetzt einfach alle möglichen Umsetzung dieses Strings in der Grammatik an:

  • ist möglich, da das Wort mit a anfängt! (wir haben noch cdf übrig)
  • danach folgt ein , also ( der erste Schritt wird dabei nochmal aufgezählt, weil er für die Verarbeitung relevant ist, um zu zeigen, wie es geparsed werden könnte.) Die markierte Struktur wird die sein, die unser Wort repräsentiert!
    • Diese Erkenntnis haben wir jetzt durch einfaches Probieren der Kombinationen erhalten.
  • ferner kommt jetzt also das d als nächster Buchstabe und auch hier führen wir wieder Schritt für Schritt alle Kombinationen durch, um zu schauen, wie / ob wir das Wort parsen können
  • Wir haben in dieser Verarbeitung jetzt 2 mögliche Varianten gefunden und es parsen können!

Damit haben wir entsprechend evaluiert, indem wir alle Regeln durchlaufen sind.

Das Prinzip nennt sich folgend:

Top-Down-Parsing

[!Definition] Top-Down-Parsing

was beschreibt das Prinzip von Top-Down-Parsing ? #card

der obige Verlauf, also das durchprobieren von allen Regeln einer Grammatik um eine Eingabe labeln bzw. erkennen zu können, wird als Top-Down-Parsing beschrieben. Dabei gehen wir also einfach alle Kombinationen durch, indem wir das Wort Schritt für Schritt betrachten und schauen, zu welcher Regel es passen oder angewandt werden könnte.

[!Tip] LL-Parsing Ferner ist der Ansatz, dass man nach und nach die Linksproduktion des Inputs durcharbeitet, dann als LL-Parsing bekannt / bezeichnet

Problem: Top-Down-Parsing wird eine exponentielle Laufzeit aufweisen Desto mehr Regeln wir haben, desto mehr müssen wir für jedes Wort probieren, was uns viel Zeit und Rechenleistung kosten kann. Ferner ist es auch polynomiell möglich, wie folgende Quelle belegen kann: link

Dennoch ist die Struktur sehr langsam!, Es brauch was besseres

[!Attention] Spoiler zum Erkennen von Grammatiken Wir werden baldig herausfinden, dass es Parser gibt, die eine Parsing-Strategie in deterministischer Struktur für Gramatiken in einer linearer Zeit konstruieren bzw. erkennen können.

Determinismus bei PDAs 112.09_kellerautomaten

Wir möchten einen deterministischen Kellerautomaten einführen - Push-Down Automata - da wir bis jetzt nur nicht-deterministische betrachtet haben ( und hierbei meinten, dass deterministische PDAs sinnfrei sind).

[!Lemma] Idee für Deterministische PDAs

was setzen wir voraus für einen deterministischen PDA? Was ist im Gegensatz zu FAs erlaubt, mit welcher Bedingung? #card

Wir nennen einen PDA bei welchem die Übergangsfunktion deterministisch ist, demnach also für jedes Tripel genau ein Folgezustand definiert und auch ein spezifisches Kellersymbol definiert ist. –> Also jede Kombination von Zustand, Eingabe und Stack-Speicher ist deterministisch definiert.

Man sieht hier schon, dass das schwieriger ist, als bei einem finite-automata, weil man jetzt den Inhalt des Stacks, sowie den aktuellen Zustand beachten muss.

Damit ist es etwa möglich, dass man ein Wort liest, aber nichts in den Stack schreibt. Also heißt das, dass hier auch -Übergänge erlaubt sind! Dabei dürfen sie aber nur einmal im Tripel auftreten.

Wir können diese Idee jetzt folgend formalisieren:

[!Definition] deterministischer Kellerautomat Ein deterministischer Kellerautomat wird durch ein 6-Tupel beschrieben, wobei die einzelnen Elemente folgende Eigenschaften aufweisen müssen:

was muss hier spezifisch für die Übergangsfunktion gelten? #card

  • beschreibt die endliche Menge von Zuständen
  • ist das Eingabealphabet
  • beschreibt das Stack-Alphabet
  • beschreibt die Menge von akzeptierenden Zuständen
  • beschreibt den Startzustand
  • beschreibt jetzt die Übergangsfunktion, wobei angibt,m dass ein Übergang nicht erlaubt ist. Bei dieser Übergangsfunktion muss jetzt gelten:

ist genau einer der unteren Übergänge definiert (also er darf nicht leer sein). Das gibt demnach folgende Kombinationen an:

[!Attention] Übergänge müssen immer genau einmal definiert sein

wan akzeptiert ein DPDA ein Wort, wenn er es eingelesen hat, wann nicht und warum? #card

Ferner gilt dadurch jetzt, dass ein DPDA ein Wort akzeptiert, wenn er nach dem Lesen des Wortes in einem akzeptierenden Zustand landet. In allen anderen Fällen wird er das Wort ablehnen -> Folgende Fälle wären das:

  • nach Lesen des Wortes sind wir nicht
  • es wird pop auf einen leeren Stack durchgeführt
  • es kommen endlose Übergänge auf, sodass wir also nicht mehr beenden.

Deterministisch Kontextfreie Sprachen

Wir haben mit der obigen Definition eines deterministischen Kellerautomaten nun eine Möglichkeit beschrieben, um bestimmte Sprachen verstehen zu können.

[!Satz] deterministisch kontextfreie Sprachen

wann nennen wir eine Sprache deterministisch kontextfrei? #card

Wir möchten eine Sprache deterministisch kontextfrei beschreiben, wenn es für sie einen DPDA gibt, der diese Sprache akzeptieren kann.

Es muss also möglich sein unter Konstruktion eines deterministischen Kellerautomaten die Sprache darstellen und erkennen zu können!

Beispiel

Betrachten wir folgende kontextfreie Sprache wie können wir dafür jetzt etwa einen DPDA konstruieren, der diese Sprache erkennen kann? #card Wir können tabellarisch alle möglichen Zustände unseres Automaten formulieren. Wir wissen grundsätzlich, dass wir hier mit dem Stack zählen wollen, wie viele Nullen auftreten, bevor wir dann anschließend diese Menge beim Zählen der Einsen danach wieder nach und nach minimieren. Wenn man Ende alles leer ist, dann haben wir entsprechend ein passendes Wort gefunden. Grafisch sieht der Automat etwa folgend aus:

und dieser wurde aus folgender Tabelle entworfen:

Input0 (Eingabe und Stack sammeln)1
Stack|0|#|0|#|0|#|
| || | || |
| | | || | |
| | | || | |
| || | || | |
Wir sehen hier, dass der Automat keinen expliziten undefined-Zustand einbindet und beschreibt, weiter aber auch nicht alle Übergänge beschrieben und definiert sind / werden.

[!Important] undefined-Zustand beim DPDA

was ist bei dem undefined-Zustand des DPDA zu beachten, sofern man ihn verwendet? #card

Wichtig ist hier, dass der neue undefined Zustand explizit genannt werden muss, (wenn er genutzt wird) weil man sonst nicht weiß, was bei Zuständen passiert, die normal nicht auftreten würden.

-> es brauch also den Eintrag, weil man somit explizit beschreibt, wie man aus diesem Zustand herauskommen kann / bzw in diesen hineinkommt.

Das ganze kann man graphisch so erweitern, wenn man das vorherige Beispiel betrachtet:

Es kann vorkommen, dass manche Übergänge, die nicht definiert sind ( beim PDA), bei nicht Nennung des undefined Zustandes einfach nicht erreicht werden dürfen!

[!Tip] Folgerung von deterministischen PDAs Wir können also deterministische Kellerautomaten bauen die Kontext-freie Grammatiken erkönnen könnten. Dabei gilt das aber nicht für alle Grammatiken!


Verallgemeinerung eines DPDAs

Man kann jetzt eine explizite Erweiterung des DPDA betrachten, welcher uns die Arbeit mit diesem vereinfachen kann:

[!Satz] Jeder DPDA hat einen Äquivalent, dass erst nach Eingabe akzeptiert/ablehnt.

Es gilt jetzt: Jeder DPDA hat einen äquivalenten DPDA, der immer den kompletten Input liest, bevor er akzeptiert oder ablehnt.

wie könnten wir das beweisen bzw. einen entsprechenden DPDA konstruieren, der das erfüllt? Warum ist diese Erweiterung sinnvoll? #card

Grundsätzlich ist es wichtig diese Verallgemeinerung vorzunehmen, um so eventuelles aufhängen eines Automaten verhindern zu können. Dieses Aufhängen kann aus zwei Gründen auftauchen:

  1. pop wird auf einem leeren Stack angewandt. Dadurch sollte sich der Zustand des Tripel eigentlich ändern, aber wir landen im gleichen Zustand, wie zuvor!
  • Verhindern kann man das, indem der Kellerboden (Stack-Anfang) mit einem # markiert wird, wobei dann jeder Zustand, der pop anwendet beim vorliegenden Symbol # entsprechend ablehnen wird bzw. in eine reject-Sackgasse übergeht. Hier liest man dann die Eingabe dennoch bis zum Ende
  1. Es treten endlose -Übergänge auf. Das heißt wir haben keine explizite Eingabe vom Stack oder der Eingabe und wiederholen uns somit wieder unendlich
  • Verhindern können wir das durch lokales Erkennen dieser Loops, welche wir anschließend auch in eine zuvor beschriebene reject-Sackgasse leiten.

Bei Beiden Fällen muss man aufpassen, dass diese Situationen nicht am Ende auftreten, bzw. muss man diese Situation etwas vorsichtiger betrachten: Der Eingang zu dieser Sackgasse kann so eventuell auch akzeptierend sein, wenn unser Übergang dahin am Ende einer Eingabe tatsächlich akzeptierend sein soll - aber das Pattern einer Schleife, die wir verbieten, hat.

Komplementbildung von DCFLs

Nachdem wir nun kontextfreie deterministische Sprachen bewiesenermaßen mit einem DPDA Automaten Darstellen können, möchten wir noch die Abgeschlossenheit dieser prüfen.

Wir betrachten hier die Komplementbildung:

[!Satz] Klasse von DCFLs ist abgeschlossen unter Komplementbildung

wie können wir das beweisen? was muss man spezifisch beachten? #card

Wir können das jetzt relativ einfach zeigen, wenn wir einen DPDA konstruieren können, der die Sprache erkennt. Ähnlich, wie bei den det. endlichen Automaten können wir hier einfach die Menge von akzeptierenden Zuständen als beschreiben, also wieder das Komplement der Zustände als akzeptierend setzen.

Haben wir also einen DPDA , dann konstruieren wir mit , wobei wir die nicht-akzeptierenden mit den akzeptierenden Zuständen vertauschen!

Problem: Wenn der Automat jetzt eine Eingabe akzeptiert, indem er nach Lesen der Eingabe eine Folge von Übergängen aufweist, würde das invertieren der akzeptierenden Zustände dazu führen, dass diese Wörter immernoch erkannt werden und wir somit nicht ganz das Komplement erhalten haben.

Lösung dazu schafft die Konstruktion der verallgemeinerung, indem also der neue Automat immer den kompletten Input liest, bevor er entscheidet. Dann ist unsere Konstruktion für richtig!

Es lässt sich hieraus noch ein Korollar definieren:

[!Korollar] CFLs können nicht DCFLs sein Es gibt also Context-Free-Languages die keine deterministisch-Context-Free-Languages sind.

wie könnten wir das beweisen? #card

Wie soeben bereits betrachtet können wir einsehen, dass es Kontextfreie Sprachen gibt, deren Komplement nicht kontextfrei ist. Eine solche Sprache wäre etwa: -> Sie ist kontextfrei aber nicht deterministisch kontextfrei.

Ferner können wir noch über die Abgeschlossenheit der Sprache(n-Gruppe) sagen:

[!Attention] DCFLs Abgeschlossenheit

DCFls sind nicht abgeschlossen unter :: Vereinigung, Schnitt, Konkatenation, der Hüllenbildung und der Umkehr

Beispiel

Wir können ferner eine Sprache betrachten, die nicht deterministisch kontextfrei ist, aber sie dennoch von einer context-freien Grammatik beschrieben werden kann - dafür aber einen NPDA benötigt! Es wird hier also die Sprache von einem Wort, das gespiegelt an sich gehangen wird, beschrieben. Die Konstruktion des NPDA könnte hierbei die Sprache akzeptieren, da man sie von folgender Grammatik erzeugen kann:

Takeaways von DPDAs

Aus den obigen Betrachtungen können wir einige Resultate, Zusammenfassungen ziehen:

[!Tip] Aspekte von deterministischen Kellerautomaten Deterministische Kellerautomaten sind die Art von Automaten, die tatsächlich umgesetzt werden können ( da hier kein nicht-det vorausgesetzt wird)

  • Sprachen von DPDAs erkannt sind deterministische kontextfreie Sprachen
  • Sie sind unter Komplementbildung abgeschlossen, jedoch nicht unter Vereinigung, Schnitt, Konkatenation, Hüllenbildung und Umkehr
  • Grammatiken, die ein DCFL erzeugen kann, haben die Form, dass sie eine eindeutige Reduktion einer Eingabe bzw eines Wortes aufweisen.

Um die Reduktion besser beschreiben zu können, möchten wir sie uns nochmal anschauen.

[!Definition] Reduktion

Grundlegend ist eine Reduktion hierbei erstmal das Inverse einer Produktion

wie können wir diese jetzt im Kontext von zwei Strings aus Variablen und Terminalen, beschreiben? Was meinen wir mit “reduziert zu “ #card

Wir sagen, dass zu reduziert wird, und beschreiben diesen Vorgang mit , wenn gilt, dass direkt zu übergeht, also

Wir sagen, dass reduziert zu , beschrieben mit , falls es eine Folge von Reduktionen gibt: Also wir können hier auf irgendeinen Pfad von Reduktionen zu unserem Wort kommen. (Es ist ähnlich zu der Produktion, wo wir durch einen Anfang und den Regeln nach und nach ein Wort produzieren können; hier verspeisen wir das Wort entlang der Regeln)

Wir nennen eine Reduktion von unter einer Grammatik jetzt, wenn es eine Reduktion von gibt, wobei die Startvariable beschreibt (Der Gramamtik).

Auch hier kann man wieder eine Linksreduktion betrachten #card

Es handelt sich um eine Reduktion der jeder reduzierte String erst dann reduziert wird, wenn alle Strings zu seiner linken bereits reduziert sind - es ist also die Rechtsproduktion aber rückwärts betrachtet!

[!Attention] Nutzen der Reduktion Mit der Regeln der Reduktion, bzw der Konstruktion dieser, beschreiben wir die Arbeitsschritte einer PDA. Wenn man also eine Eingabe unter Betrachtung der Übergänge passend zum Startpunkt reduzieren kann, dann hat man etwas gefunden, was der PDA akzeptieren kann.

Handles | Determinismus in Reduktionen schaffen

Um jetzt immer eine Eindeutigkeit beschreiben zu können, möchten wir Handles einführen, die uns dabei helfen bestimmte Umformungen zu markieren.

[!Definition] Handles Sei jetzt ein String aus Terminalen und Variablen bestehend, welcher von einer kontextfreien Grammatik erzeugt wird.

wie konstruieren wir jetzt Handles, was muss für die Konstruktion herrschen/ was resultieren sie dann? #card

Ferner sei noch ein Teilwort des Wortes , dass in einer Linksreduktion von in folgender Form mit vorliegt. Es ist also ein Teil innerhalb des Wortes, welcher notwendig ist, um es richtig zu reduzieren. Hierbei muss dieses Teilwort von einer Regel erzeugt werden, also es wird durch diese Regel verursacht.

Wir nennen jetzt einen validen String

Ferner gibt es jetzt eine Zerlegung dieses Strings von seinem zum nächsten beschrieben mit: Dabei nennen wir hier zusammen mit der Regel einen Handle von

Diese Konstruktion funktioniert weil:

  • In einer Linksreduktion das Suffix immer aus Terminalen besteht - Variablen müssten von der vorhergehenden Reduktion zur rechten von stammten was nicht erlaubt ist.
  • Ein valider String kann potentiell mehrere Handles haben. Wenn das so ist dann ist die Grammatik nicht eindeutig

Diese fehlende Eindeutigkeit müssen wir verhindern, denn dadurch verlieren wir die Möglichkeit einfach und schnell parsen zu können!

Beispiel | Handles

Betrachten wir ferner ein Beispiel, was Handles in einer Grammatik verwendet:

[!Example] Betrachten wir folgende Grammatik: Sie weißt die Sprache auf. Betrachten wir jetzt exemplarisch ein paar Wörter und ihre Reduktion: aaabbb kann man zerlegen in: aaabbbbbb kann man zerlegen in: Beide Reduktionen konnte man umsetzen, aber zu Beginn steht links immer der gleiche String. –> Die Handles sind unterschiedlich, obwohl sie gleich aussehen (ohne Kontext!)

Das heißt ferner, dass ein NPDA diese Ersetzungen durchführen kann, weil dann der Ausführungsbaum alles probiert, aber ein deterministischer Automat müsste erst entscheiden, welche Ersetzung er tätigt. Man könnte ihn somit falsch lesen lassen: so haben wir ihn, obwohl das Wort passen sollte, so falsch aufsplitten lassen, dass das Wort nicht erkannt wird!

Wir sehen hier, dass bei einem deterministischen DPDA Wörter falsch reduziert bzw nicht reduziert werden könne, wenn Terme auf der Linken Seite teils gleich aussehen und somit mehrere Regeln diese Struktur verarbeiten könnten ( sie sehen also nicht ganz eindeutig aus und somit kann unser DPDA sie falsch reduzieren).

Wie können wir dieses Problem nun beheben? #card Es lässt sich hier im Beispiel damit lösen, dass man ein Präfix einbringt, was eindeutig festlegt, wie bzw. welcher Form das Wort folgen wird, damit diese Uneindeutigkeit verloren geht. wird quasi der neue Start und somit wird direkt am Anfang gesagt, wie reduziert wird. betrachtet man die falschen Wörter von oben folgt: sind nicht mehr gleich, weil sie jetzt eindeutig bestimmt sind am Anfang!

Diese Idee können wir jetzt formalisieren:

Forced handle

[!Definition] Forced Handles Wir beschreiben einen Handle eines validen Strings als erzwungen (forced), wenn gilt:

was muss gelten? #card

ist der einzige Handle in allen Validen Strings der Form -> Also es ist eindeutig bestimmt, weil es in jeder Variation eines Strings, der rechts davon ist, dennoch eindeutig ist. (der Linke Term vom Handle ist egal, weil der schon bearbeitet wurde)

Damit können wir jetzt also formalisiert sagen, wann eine Grammatik als deterministisch kontextfreie Grammatik beschrieben werden kann.

DCFGs

[!Definition] Deterministisch Kontextfreie Grammatik für forced handles. Eine deterministische kontextfreie Grammatik ist eine kontextfreie Grammatik in der jeder valide String einen Forced handle hat.

wie kann man also herausfinden, wob eine CFG deterministisch ist oder nicht? Was brauch es dazu? #card

Für jede CFG existiert ein \\DFA// welcher die Handles erkennen kann. Dabei akzeptiert dieser Automat nur dann ein Wort wenn:

  1. ist ein Präfix eines validen Strings und
  2. endet mit einem Handle von und das so, dass der akzeptierende Zustand von auf die Handles ( also die möglichen Handles) erkennen kann / identifiziert.

Demnach folgt für uns jetzt:

Ein DFA kann die Handles einer CFG erkennen.

Eine CFG ist ferner genau dann deterministisch, wenn jeder akzeptierend Zustand unseres Automaten dann genau einem Handle entspricht. Dabei ist auch wichtig, dass jeder akzeptierende Zustand keinen ausgehenden Pfad zu einem anderen akzeptierenden Zustand hat (haben darf). -> Die Handles sind also forced

Konstruktion eines CFG-erkennenden DFA

Wir wollen nun also den zuvor benannten DFA bauen, der die Handles einer CFG erkennen kann und somit entscheidet, ob eine CFG DFCG ist oder nicht!

Wir wollen zuerst einen nicht-deterministischen Automaten bauen, da es die Betrachtung einfacher macht!

[!Lemma] Konstruktion der DK-Tests

Sei jetzt eine CFG. Dann bauen wir jetzt einen NFA (nicht det, weil es einfacher zu Konstruieren ist, wir können ihn ja später zu einem DFA umwandeln) welcher alle Eingaben von der Form akzeptiert, wobei das Suffix beschreibt, für dass es in der Sprache eine Regel gibt. Dieser Ausdruck ist regulär

wie konstruieren wir mit dieser Information jetzt einen NFA? #card

Wir können jetzt daraus einen NFA bauen, welcher alles annimmt und schaut, ob er anschließend in das Suffix übergeht. Dafür muss er den Fortschritt mit einzelnen Zuständen “Speichern” (er hat ja keinen Stack) und somit können wir dann den Automaten folgend aufbauen: Da es sich um einen NFA handelt, muss bei unserer Berechnung mindestens ein Blatt des Berechnungsbaums akzeptiert werden. Um das umsetzen zu können setzen wir jetzt folgende Startbedingungen des NFA:

  • erhält einen Startzustand, welcher mit Übergängen zu allen Zuständen der Form übergeht ( also alle Suffixe die unsere Grammatik zulassen kann)
  • Da wir jetzt zu jeder Grammatik einen Übergang haben, kann unser NFA entsprechend schauen, wie bzw. ob eine Regel getroffen wird, wenn eine Eingabe getätigt wird.

Erweiterung des NFA Wir können diesen Automaten jetzt noch verfeinern, indem wir jetzt mit Handles arbeiten und somit spezifischer die Zustände bzw. die Regeln der Grammatik finden und akzeptieren können.

[!Definition] Erweiterung der DK-Tests um Handles, Reduktion

Wir betrachten erneut den Grundlegenden NFA welcher die Wörter mit Suffix Regel der Grammatik akzeptiert, dabei wird dann Schritt für Schritt ein Zustand für jeden Verarbeitungsschritt gesetzt, welcher am Ende akzeptierend sein solte. Das haben wir mit einem Startzustand welcher mit einem -Übergang zu allen von diesen Regel-erkennenden Strängen übergeht, geschafft.

Wir wollen jetzt den Automat un Handles und um Reduktions Übergänge erweitern, damit wir entsprechend die Grammatik erkennen könne.

wie gehen wir vor, sodass unser DFA Handles und Reduktionen der Grammatik erkennen kann? #card

Für diese Konstruktion nehmen wir also den gleichen Automaten wieder.

  • Mit gleichen Zustände, wie , jedoch nun einen Startzustand welcher die Startvariable der Grammatik wiedergibt. Anschließend führen Übergänge zu allen Teilpfaden, die dann Regel erkennen soll/wird.
  • Es werden shift-Übergänge übernommen, die quasi einen Terminal unter Anwendung einer Regel durchlaufen, also
  • Wir fügen aber auch noch reduce-Übergänge ein, welche Reduktionen bei einer Eingabe versuchen. Das heißt, dass sie mit -Übergängen folgende Reduktionen probieren: und somit:
  • In dieser Betrachtung sind dann alle matched Zustände der Form akzeptierend,sie haben keine augehenden Pfade mehr, sind also beendet!

Es folgt daraus dann, dass man mit diesem Automaten valide Handles bzw valide Strings finden kann.

[!Lemma] DK-Test erkennt valide Strings

warum kann unser DK-NFA jetzt valide Strings der Form handle erkennen? #card

Der neu konstruierte Automat geht auf einer bestimmten Eingabe in einen akzeptierenden Zustand, genau dann, wenn darstellt und ein Handle für einen validen String mit einer Regel ist.

Konvertierung des NDA in DFA

Wir möchten ferner den NDA in einen DFA umwandeln, damit wir eine Struktur erhalten, die realistisch umsetzbar ist!

[!Satz] Konstruktion eines DFA auf Grundlage des NFA

Sei eine CFG. Wir möchten jetzt einen DFA konstruieren, der die Handles der Grammatik erkennen kann.

wie können wir vorgehen (wenn wir den NDA schon kennen?) Wie ist aufgebaut? Wie baut man daraus dann einen Tester? #card

Wir verwenden die Konversion eines NDA zu einem DFA Übersetzung von NFA zu DFA und werden hier anschließend solche Zustände entfernen, die wir eh nicht erreichen können (Minimierung)

Damit hat unser Automat dann Zustände, die jeweils eine oder mehrer items (was Zwischenschritte bei einer Erkennung einer Regel waren) enthalten (Die Zustände sind jetzt Potenzmengen!). Dadurch gilt dennoch, dass ein akzeptierender Zustand mindestens eine abgeschlossene Regel ist.

[!Lemma] -Test bauen und prüfen, ob eine Grammatik Deterministisch ist

Wir haben jetzt definiert, wie man einen Automaten bauen kann, der ein DFA ist und Handler einer Grammatik (CFG) erkennen kann. Wie finden wir heraus, ob die Grammatik deterministisch ist? #card

Mit dieser Konstruktion können wir jetzt den Tester erweitern/bauen:

  1. Wir bauen wie obig beschrieben den Automaten
  2. Wir analysieren die Zustände des Automaten: Wir sagen, dass er deterministisch ist, wenn folgende zwei Punkte gelten:
  3. es gibt genau eine abgeschlossene Regel in einem akzeptierenden Zustand
  4. ein akzeptierender Zustand hat keine weitere Regel (also eine zweideutige Fortsetzung zu einer anderen Reduktion)

Konstruktion eines DPDA unter Anwendung von

Mit dem obigen Konstrukt können wir jetzt den deterministischen Kellerautomaten bauen, welcher dann eine Produktion einer Grammatik erkennen und reduzieren kann. (er kann sie also lesen und produzieren).

Folgend möchten wir den Ablauf dafür beschreiben:

[!Definition] Konstruktion DPDA aus

Wir wollen jetzt die Idee des DFA verwenden, um eine Produktion/Reduktion durchführen zu können.

Wie können wir jetzt eine Reduktion durchführen und anschließend das Wort adäquat weiterverarbeiten? Warum brauch es den Stack? #card

Wir folgen einem Schema:

  1. Wir konstruieren wieder den DFA , wie obig beschrieben. Wir wissen, dass wenn akzeptiert, dann zeit sein Zustand auf ein Handle der Reduktion. –> Ferner sind alle Reduktionen erzwungen und sie können beim Verarbeiten der Eingabe von links ausgeführt werden. Dabei muss man den Rest des Inputs noch nicht lesen.
  2. Wir bauen unseren DPDA also so, dass er einfach simuliert. Wenn DK das erste mal akzeptiert, dann müssen wir die Reduktion umsetzen und eigentlich von vorne Anfangen –> Das geht aber nicht, weil wir den Input schon verloren haben (beim DFA). Hier jetzt einfach die Eingebae im Stack zu speichern ist auch nicht möglich, weil wir sonst bei einer Reduktion den ganzen Stack popen müssten –> damit aber die Eingabe verlieren xd

Lösung: Statt der Eingabe speichern wir die Zustände des Automaten .

  1. Immer wenn (DPDA) ein Symbol aus liest, schreibt er dann den entsprechenden Zustand von DK auf den Steck ( da ist dann auch der derzeitige Teilstring enthalten).
  2. Wenn jetzt eine Reduktion erkennt, dann popt der DPDA einfach Symbole vom Stack –> er liest dann den letzten Zustand von auf dem Stack und fährt dann mit der Vearbeitung von diesem Punkt fort.
  3. Wenn jetzt der DPDA am Ende von ist und die Startvariable auf den Stack schreibt (also quasi passend reduzieren konnte!), dann akzeptiert er!
  4. Dieser akzeptierende Zustand zeigt die deterministische Ableitung von !

Beispiele für Parser

  • yacc (yet another compiler compiler) ist ein Parser (compiler) von Stephen C. Johnson welcher auf dem Paper von Knuth basiert und für UNIX entwickelt wurde. Er ist ein Compiler-Compiler für die erste C++ Version!
  • bison (GNU yacc) und Berkley Yacc sind open-source Erweiterung von yacc, von Bjarne Stroustup
  • tree-sitter ist ein moderner Parser-Generator für Dinge, wie Syntax-Highlighting in Atom,VSCode etc. BSP: Tree-Sitter Grammatik für:

Folgerungen | Erkenntnisse

Unser betrachteter Parser liest immer eine Eingabe von links nach rechts. Er führt dabei Rechtsableitungen durch - in der Produktion wird von abwärts jeweils rechts ersetzt. Dabei schaut er 0 Zeichen in die Zukunft, erkennt also direkt forced handles. ->> Daher nennen wir sie auch !

  • Wir haben auch gesehen, dass unser DFA aus shift/reduce-Schritten besteht. Ein Parser dieser Struktur heißt shift-reduce-Parser

  • Der Zwang zu forced handles schränkt die Grammatik natürlich stark ein! Daher gibt es Erweiterungen, bei denen handles erst zwingend werden, wenn schon Schritte nach rechts vorausschgeschaut werden kann. Man nennt sie dann Grammatiken!

[!Tip] Äquivalenz von -Grammatiken

Alle -Grammatiken sind Äquivalent –> man kann sie in eine Grammatik übersetzen.

Man nennt sie jetzt kanonische-LR-Parer oder Dadurch wird eine Grammatik u.U. aber auch sehr groß! Weiterhin sind alle Grammatiken deterministisch!