 Ja, herzlich willkommen zum sechsten und letzten Teil innerhalb dieses Themenblocke-Syntax im Rahmen der Veranstaltung Programmierparadigmen. Was wir bis jetzt gemacht haben, ist uns Pasa anzuschauen, die top-down funktionieren. Und nun hier im letzten, dieser letzten Einheim wollen wir uns auch mal bottom-up Pasa anschauen. Also die, die den Pastry nicht von oben nach unten, sondern Schritt für Schritt von unten nach oben aufbauen. Die Klasse von Pasa, die man dafür verwendet, hatten wir ja in einer der früheren Einheiten schon kurz erwähnt, nämlich sind das die LRK Pasa. Noch mal zur Erinnerung, wofür diese Abkürzung denn genau steht. Also das L bezeichnet die Reihenfolge, in der die Tokens eingelesen werden. Und wie bei den top-down Pasa, die wir bis jetzt gesehen haben, geht es hier auch wieder von links nach rechts. Also das Scanning geht von links nach rechts. Das R in dieser Abkürzung steht dafür, dass wir zuerst die Ableitungen wählen, die auf der rechtesten Seite innerhalb unseres zu erstellenden Passbaumes ist. Das ist also der zweite Buchstabe und das K, genauso wie auch schon bei den LLK Pasa, die wir in der letzten Einheit gesehen haben, steht dafür, wie viele Tokens der Pasa vorher schaut. Also ein LRK Pasa z.B. würde immer eintoken, vorausschauen, um dann zu entscheiden, wie genau er weiter verfährt. Allgemein gilt das die bottom-up Pasa relativ schwierig per Hand zu implementieren sind und ich werde das nicht empfehlen zu tun, zumindest nicht für etwas komplexere Kramatiken. Stattdessen werden die in der Regel automatisch generiert und zwar wieder mit Hilfe einer Tabelle, die wir uns gleich noch ein bisschen genauer anschauen werden. Der Algorithmus, den wir uns hier als ein Beispiel für einen bottom-up parsing Algorithmus ein bisschen genauer anschauen werden, nennt sich Shift Reduce Algorithmus und der Name kommt daher, dass dieser Algorithmus im Grunde zwei Operationen immer wieder ausführt, nämlich Shift und Reduced. Das Ganze funktioniert so, dass wir eine Schleife haben, in der nach und nach Tokens eingelesen werden und dieser Pass Tree aufgebaut wird, bis wir schlussendlich beim Stadtsymbol der Kramatik oben ankommen. Die zwei Operationen, die dabei immer wieder ausgeführt werden, sind zum einen das Shift, was so viel bedeutet wie, dass wir in dem Input Stream aus Tokens einen Schritt weitergehen, also einen weiteren token einlesen und zum anderen das Reduce, was bedeutet das mithilfe der bereits eingelesenen Tokens und mithilfe der bereits generierten Teilbäume, die jeweils ein Nicht-Terminal als ihre Wurzel haben, eine weitere Produktionsregel der Kramatik angewandt wird, um somit Schritt für Schritt einen größeren Baum aufzubauen. Schauen wir uns das Ganze am besten mal anhand eines Beispiels ein bisschen genauer an. Also das wird ein Beispiel für Shift Reduce Parsing und zwar für folgende Kramatik. Die Kramatik hat drei Nicht-Terminalen, nämlich S, was auch das Stadtsymbol ist und Ableitbar ist nach A, T, R und E. T ist Ableitbar nach T, B, C oder einfach nur dem Terminal B und R ist Ableitbar nach D. Wenn wir jetzt einen konkreten Input haben, zum Beispiel folgenden hier, der besteht aus A, B, B, C, D, E, dann wird der Pasa, wenn er diesen Shift Reduce Algorithmus ausführt, folgende Schritte machen. Wir fangen zunächst an, Inputs einzulesen. Also der erste Schritt ist immer Shift. In dem Fall lesen wir also das A ein. Was bedeutet, wir schreiben das jetzt einfach mal hier unten hin. Nur mit diesem A können wir noch keine Reduce Operation ausführen, denn es gibt keine Regel, die nur ein A auf der rechten Seite hast. Das heißt, der Pasa führt nochmal Shift aus und liest jetzt auch noch das B ein. In dem Moment kann die erste Regel angewandt werden, nämlich indem wir die zweite Regel für T verwenden, die sagt ein T kann nach B abgeleitet werden. Und wir drehen sozusagen diese Regel hier um, indem wir das B nehmen und zu einem T nicht-Terminal verallgemeinern. So, was der Pasa als nächstes tut, ist weitere Shift Operationen ausführen, indem er einfach weitere Tokens ein liest. Zunächst hier das nächste B, das heißt, wir haben jetzt das zweite B drin und weil das noch nicht ausreicht, um weiter zu reduzieren, auch noch das folgende, nämlich das C. Das ist jetzt wieder genug, um eine Regel anzuwenden, und zwar wieder eine Regel für T, das ist jetzt mal nicht die zweite, sondern die erste, die sagt T, kann abgeleitet werden nach T, B und C. Und zwar nehmen wir jetzt diese drei Symbole, die wir hier haben, das nicht-Terminal T, was wir vorhin ja erstellt haben, und die beiden Bs aus dem, das B und das C aus dem Inputstream und verallgemeinern das in ein weiteres T. Jetzt geht es weiter mit einem weiteren Shift, also einem weiteren Einlesen eines Tokens, nämlich dieses D. Und wenn wir das gemacht haben, Siehe, da können wir wieder eine Regel anwenden, nämlich die Regel, die sagt, dass R nach D abgeleitet werden kann, indem wir jetzt diese Regel wieder umdrehen und das D zum R verallgemeinern. Jetzt bleibt noch ein weiterer Token einzulesen, also nochmal Shift und zwar für dieses E. Und damit das Ganze jetzt aufgeht, muss jetzt alles reduziert werden, vielleicht darüber mehrere Schritte, bis wir schlussendlich beim Startsymbol ankommen. Ansonsten wäre dieser Baum kein valider Pass-Tree, und das heißt, der Input wäre kein valider Input entsprechend unserer Grammatik. In dem Fall geht es aber auf, nämlich indem wir die Regel für S anwenden, die sagt, S kann abgeleitet werden nach A, T, R und E. Denn wir haben ein A, wir haben ein T, an der Wurzel eines Baumes, eine R auch an der Wurzel eines Baumes und ein weiteres E. Und wenn wir die jetzt alle zusammenführen, bekommen wir genau den Baum raus, den wir hier erwarten und haben damit gezeigt, dass dieser Input tatsächlich legal entsprechend unserer Grammatik ist. Jetzt haben wir das Ganze am Beispiel gesehen und die Frage ist nun, wie können wir automatisch einen solchen Paser erstellen für eine bestimmte Grammatik? Die Idee ist hier wieder, dass es so ähnlich gemacht wird wie in den Tabellenbasierten Scanners und im Tabellenbasierten Top-Down-Paser, den wir bereits in früheren Einheiten gesehen haben, nämlich mithilfe von vorberechneten Tabellen, die für eine bestimmte Grammatik berechnet werden. Und dann von einem generischen Algorithmus bearbeitet werden. In diesem Fall gibt es nicht nur eine Tabelle, sondern zwei, die Action-Table und die Go-to-Table. Die Action-Table gibt uns an, welche Aktion der Paser als nächstes ausführen soll. Und da gibt es vier Möglichkeiten, nämlich Reduce, also das Reduzieren von Nicht-Terminalen und Tokens zu weiteren Nicht-Terminalen. Schifft, also das einlesen, weitere Tokens, except was bedeutet, dass wir fertig sind mit dem Paasen aller Tokens und der eingelesene String tatsächlich als valides Programm akzeptiert wird. Oder eben Arrow, wenn das nicht der Fall ist und der Paser nicht weiter kommt und deshalb die Eingabe leider verwerfen muss. Die zweite Tabelle ist die Go-to-Table, die uns angibt, wie wir durch die verschiedenen Zustände, die der Paser intern hat, durchlaufen. Beide Tabellen nehmen einen Zustand, nämlich den aktuellen Zustand als den Input. Die Action-Table liest außerdem ein weiteres Token ein, wo ihn gegen die Go-to-Table anschaut, welches Nicht-Terminal gerade noch bearbeitet wird und dann entsprechend den nächsten Zustand ausgeben. Das Ganze wird noch kontrolliert von einem Stack, den dieser Paser aufbaut und führt. Und was auf diesem Stack drauf sind, sind immer Paare, nämlich Paare aus Symbolen und Zuständen. Und was dieser Stack anders macht als der Stack, den wir beim Top-Down-Pasen gesehen haben, ist, dass hier im Stack gespeichert wird, was der Paser bereits in der Vergangenheit gesehen hat. Also all die Teilbäume zum Beispiel, die bereits aufgebaut wurden, wohingegen beim Top-Down-Pasen der Stack hier angegeben hat, was wir in der Zukunft noch erwarten zu sehen. Hier ist mal ein Beispiel für so eine Tabelle. Und zwar beinhaltet diese Tabelle, diese beiden Tabellen, die wir gerade angesprochen haben, also sowohl die Action-Table, das ist der mittlere und der linke Teil, als auch die Go-To-Table, die hier so auf die beiden äußeren Teile aufgeteilt dargestellt ist. Was sagen uns jetzt diese Tabellen, fangen wir mal an mit der Action-Table. Also für jeden Zustand, in dem wir sind und für jedes Token, das wir als Nächstes lesen, steht drin, welche Aktion auszuführen ist. Zum Beispiel bedeutet eine Aktion, die mit einem S losgeht, dass wir schiften sollen und anschließend zu einem anderen Zustand, also in dem Fall Zustand 1, wechseln sollen. Wenn stattdessen eine Aktion drin steht, die mit einem R losgeht, bedeutet, dass wir wollen die Reduce-Operation ausführen und dafür eine bestimmte Regel benutzen, die durch die Ziffer hinten dran angegeben ist, also in dem Fall die dritte Regel in unsere Grammatik. Falls EXCEPT da steht, bedeutet, dass wir sind fertig mit dem Pasen des Inputs und das passiert hier in dem Beispiel in dem Moment, wo wir End-of-File im Zustand S7 erreichen. Und dann gibt es schlussendlich noch den Fall, dass das gar nichts drin steht. Also wenn jetzt zum Beispiel Zustand S5 auf End-of-File treffen würde, dann steht hier nichts und nichts bedeutet in dem Fall, dass der Pasen nicht weiß, was er machen soll und deshalb ein Fehler ausgibt. Was man hier auch noch sieht, ist die andere Seite der Tabelle, also die Go-to-Tabelle. Und die beschreibt einfach nur, in welche Zustände erreicht oder zu welchen Zuständen man gehen soll, wenn in einem bestimmten Zustand ein bestimmtes Nicht-Terminal auftritt, in dem Fall zum Beispiel dann zum Zustand 2. Wenn wir so eine Tabelle haben, können wir dann mit einem generischen Algorithmus das eigentliche Parsing durchführen, und zwar indem wir immer in dieser Tabelle nachschauen, welche Aktion als nächstes durchgeführt werden muss und in welchem Zustand der Pase sich als nächstes begeben muss. Dieser Algorithmus ist hier mal gezeigt, was man dabei sieht, ist, dass es zum einen diesen Stack gibt, dass alle die bereits eingelesenen Fragmente des zubauenden Parsetrees beinhaltet. Auf dem Stack sind immer Paare von Symbolen und Zuständen und ein Symbol kann entweder ein Terminal oder ein Nicht-Terminal sein. Initial pushen wir erstmal Enter-File und den Zustand 0 drauf, lesen dann den nächsten Token ein und dann geht es in diese Hauptschleife rein, in der das eigentliche Parsing stattfindet, wo wir dann jeweils das oberste Element des Stackes anschauen. Je nachdem, was unsere Action-Table uns jetzt sagt, was wir damit machen sollen, wird dann entsprechend verfahren. Also wenn zum Beispiel in der Action-Table steht für diesen Zustand, der jetzt gerade oben auf dem Stack ist und diesen nächsten Token, den wir eingelesen haben, sollen wir Reduce durchführen, und zwar mit dieser Regel, die X nach Y1 und so weiter bis Ynm ableitet. Dann wird das gemacht und zwar in dem M Paare vom Stack runtergenommen werden, also das M ist das M, was wir auch hier haben. Das heißt, wir suchen also diese Teilbäume oder auch Tokens, die schon in der Vergangenheit gesehen wurden und holen die vom Stack wieder runter und pushen dann ein neues Element auf den Stack, und zwar je nachdem, wo uns die GoTo-Table hinschickt und außerdem als Symbol dieses Nicht-Terminal, was wir mit der Reduce-Operation erstellt haben. Der andere Fall ist, dass wir durch die Action-Table gesagt bekommen, dass wir für den aktuellen Zustand und den Token, den wir gerade sehen, Shift ausführen sollen. Das heißt, wir lesen einen weiteren Token ein, der dann hier in, der uns dann schlussendlich in den Zustand Sstrich führt. Dann gibt es noch den Fall hier unten, dass wir von der Action-Table gesagt bekommen, dass wir die Token-Sequenz komplett eingelesen haben und den Input so akzeptieren sollen und dann macht der Algorithmus genau das. Und schlussendlich gibt es auch noch den Fall hier drüben. Wenn nämlich keine der Aktionen, die wir gerade beschrieben haben, möglich ist, dann heißt das, wir können nicht weiter pausen und es gibt einen Fehler. Eine Frage, die Sie jetzt vielleicht stellen könnten, ist ja, wo bekommen wir denn genau diese Tabelle her? Und im Gegensatz zu den Top-Down-Pasern, wo wir das berechnet der Tabelle ja sehr ausführlich angeschaut haben und auch an Beispielen durchgerechnet haben, wenn wir das hier nicht tun. Ganz einfach, weil das ein bisschen über diese Veranstaltung hinausgeht. Wenn es sich dafür interessiert, kann gerne ein bisschen mehr dazu lesen, dass das Stichwort hierfür ist Characteristic Finite State Machine. Das ist ein endlicher Automat, der speziell für die Grammatik abgeleitet werden kann und mithilfe derer oder dessen Mann dann schlussendlich diese Tabelle auch berechnen kann. Die Details hängen auch ein bisschen davon ab, was genau für ein Bottom-Up-Pase wir haben. Es gibt da nämlich nicht nur ein, sondern eine ganze Reihe und ich will einfach bloß mal die Namen von drei, die relativ häufig vorkommen, falls es mal jemand irgendwo hört, dass man das ein bisschen einnauten kann. Also es gibt SLA-Paser, das steht für Simple LA. Es gibt LA, was für Lookahead LA steht und außerdem zum Beispiel auch noch Full LA-Paser. Wie die jetzt alle ganz genau funktionieren, geht wie gesagt über das, was wir hier machen hinaus und für den Zweck dieser Veranstaltung genügt ist, das zu verstehen, was wir bis jetzt hier dran hatten. So und zum Abschluss dieser Einheit noch ein kleines Quiz, was wieder überprüft, dass zumindest ein paar Sachen hängen geblieben sind von dem, was ich hier erzähl. Das Quiz sieht wieder so aus, dass ich vier Aussagen hier aufgeschrieben habe, von denen manche richtig sind und manche falsch. Und Sie sollen bitte in Ilias abstimmen, welche diese Aussagen denn tatsächlich stimmen. Dazu bitte das Video kurz anhalten, abstimmen und anschließend dann die Lösung anhören. So, schauen wir uns mal die Lösungen an. Also der erste Satz, der hier steht, ist falsch. Denn Recursive Descent-Paser bauen den Pastry eben nicht von unten nach oben, sondern von oben nach unten auf. Der zweite Satz hingegen ist richtig, denn das K in LRK steht tatsächlich dafür, wie viele Tokens im Voraus gelesen werden, nämlich genau K. Der dritte Satz stimmt fast, aber leider ist die Reihenfolge falsch, denn wir brauchen First and Follow-Sets, um Predict-Sets zu berechnen und nicht andersrum. Und schlussendlich der letzte Satz stimmt auch, denn der Stack in einem Top-Down-Paser enthält tatsächlich diese Bole, die in der Zukunft noch erwartet werden, wohingegen bei einem Bottom-Up-Paser der Stack eben diese Bole enthält, die wir bereits eingelesen haben. Ja, und dann sind wir auch schon am Ende dieses Themenblockesöntags, in dem wir uns eine ganze Reihe Sachen angeschaut haben, nämlich zum einen, wie können wir überhaupt Programmiersprachen spezifizieren, mithilfe von regulären Ausdrücken, die beschreiben, wie die Tokens aussehen und mithilfe von kontextfreien Grammatiken, die uns dann beschreiben, wie wir diese Tokens zu legalen Programmen zusammensetzen können. Und dann haben wir uns die Tools angeschaut, die tatsächlich diese Überprüfung machen, ob ein gegebener String oder eine gegebene Tokenssequenz legal ist, nämlich die Scanner und die Paser. Und damit haben Sie jetzt, glaube ich, ein ganz gutes Verständnis dafür, wie die Synthags einer Sprache überhaupt spezifiziert wird und wie ein Compiler oder auch andere Programmanalysetools dann überprüfen, ob Programme tatsächlich dieser Spezifikation entsprechen. Vielen Dank für's Zuhören und bis zum nächsten Mal.