 Ja, herzlich willkommen zur fünften Einheit im Themenblock Sündachs in der Veranstaltung Programmierparadigmen. Was wir heute und hier machen wollen, ist uns weiterhin mit Top-Down-Parser beschäftigen, die wir ja in der letzten Einheit schon angefangen haben, uns anzuschauen. Was wir in der letzten Einheit gemacht hatten, waren Recursive-Descent-Parser anzuschauen, also eine einfache Art von Top-Down-Parser, die man üblicherweise per Hand schreibt. Was wir jetzt hier machen wollen, ist uns anzuschauen, wie man solche Top-Down-Parser automatisch generieren kann, so dass wir eine gegebene Grammatik nehmen und daraus automatisch den passenden Parser generieren können. Ja, wie kann das Ganze funktionieren? Also im Grunde ist das Top-Down-Parsing ja einfach, wir nehmen die Grammatik, fangen mit dem Stadtsymbol der Grammatik an und expandieren dann ein Nicht-Terminal nach dem nächsten, bis wir nur noch Terminale übrig haben. Die große Herausforderung dabei ist, dass wir manchmal mehrere Optionen haben, also dass ein Nicht-Terminal nach verschiedenen Dingen abgeleitet werden kann und der Pase dann irgendwo herwissen muss, welche diese Optionen er jetzt wählen kann. Und genau um diese Entscheidung treffen zu können, werden wir eine Menge für jeden Nicht-Terminal berechnen, nämlich das Predict-Set, also die Predict-Menge, die uns sagt, wenn dieses bestimmte Token als nächstes kommt, dann müssen wir diese Regel nehmen und wenn dieses andere Token als nächstes kommt, dann müssen wir eben diese Regel benutzen. Und um diese Predict-Sets zu berechnen, werden wir zwei Hilfsmengen berechnen, nämlich First und Follow. Was die Intuitiven machen ist das folgende, das First-Set eines Nicht-Terminals sagt uns, welche Terminale denn als erstes kommen können, wenn wir das Nicht-Terminal N expandieren und Follow von N sagt uns, welche Terminale nach dem Nicht-Terminal N als nächstes kommen können. Und mithilfe dieser beiden Hilfsmengen First und Follow können wir dann dieses Predict-Set berechnen, was uns schlussendlich hilft den Pasa automatisch zu generieren. So, dann schauen wir uns diese First und Follow-Sets mal ein bisschen genauer an und bevor wir die genaue Definition anschauen, erst mal ein kleines Beispiel, um die Intuition besser zu verstehen. Also First von A soll alle Terminale enthalten, die am Beginn einer Ableitung dieses A kommen können. Das kleines Beispiel haben wir hier unten mal eine wirklich minimale Grammatik mit nur einer einzigen Regel, die nämlich sagt, das Symbol S, was auch gleichzeitig das Stadtsymbol ist, kann entweder nach Simple abgeleitet werden oder nach Simple und dann weitere Dinge, die es S abgeleitet werden können, gefolgt von N. Das First-Set von S enthält nun also alle Tokens oder Terminale, die als nächstes kommen oder als erstes kommen können, wenn wir S ableiten. Und in diesem Beispiel sind es genau zwei Möglichkeiten, nämlich Simple und Beginn und dementsprechend sind die dann auch das First von S. So, dann schauen wir uns mal an, wie diese First-Sets im allgemeinen Fall berechnet werden. Also der einfachste Fall ist, dass wir das First-Set von A dem leeren Wort berechnen wollen, was trivialerweise wieder das leere Wort selbst ist, weil alles, was wir aus dem leeren Wort ableiten können, ist natürlich auch wieder nur das leere Wort. Der etwas interessantere Fall ist, dass wir das First-Set berechnen möchten von einer Sequenz von Terminalen und Nicht-Terminalen, die ich hier mal mit A Alpha bezeichne, wobei das A einfach das erste Zeichen in dieser Sequenz von Terminalen und Nicht-Terminalen ist und Alpha die restliche Sequenz ist. Und wenn wir jetzt so ein A und Alpha haben, von dem wir das First-Set berechnen wollen, dann gibt es mehrere Fälle. Der einfachste Fall hierin ist, dass unser A selbst ein Terminal ist und in dem Fall ist das First-Set von A Alpha dann ganz einfach dieses Terminal, weil wir sicher wissen, dass als erstes immer dieses Terminal A kommt. Falls das A selbst kein Terminal ist, müssen wir in das A selbst reinschauen und das First vom A berechnen. Und jetzt gibt es aber noch den Fall, dass das A selbst nach Epsilon ableitbar ist und sozusagen zu nichts wird und in dem Fall müssen wir uns anschauen, was aus dem Alpha werden kann, zusätzlich zu dem, was aus dem A abgeleitet werden kann. Das heißt, wir würden auch wieder das First von A anschauen. Falls das aber nach Epsilon abgeleitet wird, interessiert uns das an der Stelle nicht und das Ergebnis davon wird dann vereinigt mit dem First-Set von Alpha, um eben den Fall abzudecken, dass A nach Epsilon abgeleitet wird. Das ist also für den Fall, dass es eine Ableitung gibt, wo A schlussendlich zu Epsilon wird. Dieses Ableitungssymbol mit dem Sternchen dran heißt einfach nur, dass es eine Sequenz von Ableitungen gibt, die schlussendlich zu Epsilon führt. Und falls die oberen beiden Möglichkeiten nicht der Fall sind, also sozusagen der dritte oder der default Fall hier, nehmen wir einfach das First vom A, denn dann wissen wir, dass A weder ein Terminal ist, noch nach Epsilon abgeleitet werden kann und das heißt First von A Alpha ist einfach dasselbe wie First von A. Wenn wir diese Definition oder diese Berechnungsvorschrift jetzt anschauen, ist die Frage, wie wenden wir das tatsächlich auf eine Grammatik an? Und das funktioniert eigentlich ganz einfach. Also wenn wir eine gegebenen Grammatik haben, für die wir nun die First-Sets ausrechnen wollen, dann wenden wir einfach immer wieder diese Regeln an und zwar rekkusiv, bis alle First-Sets konstant bleiben, also sich nicht mehr ändern. So und mit dieser Berechnungsvorschrift oder diesem kleinen Algorithmus hier können wir die First-Sets nun für beliebige Grammatiken ausrechnen. Schauen wir uns das Ganze mal an einem Beispiel an. Wir werden zwei Beispiele sehen und das dritte dürfen Sie dann als Quiz selbst machen. Das erste Beispiel ist eine kleine Spielgrammatik mit einer Reihe von Regeln, die ich jetzt hier mal aufschreibe. Also unser Stadtsymbol ist S und S kann abgeleitet werden nach A, S und E, wie so meistens auf meinen Folien sind die Großbuchstaben, die nicht Terminale und die kleinen Buchstaben sind die Terminale oder die Tokens der Programmiersprache dann. Das S kann aber auch abgeleitet werden nach A Groß B und Groß B hat dann mehrere Optionen. Es kann entweder abgeleitet werden nach Klein B, nochmal Groß B und dann E oder nach Groß C und Groß C kann abgeleitet werden nach Klein C, Groß C, Klein C oder nach D. So, das ist die Grammatik und jetzt wollen wir für jedes der nicht Terminale in dieser Grammatik das First Set ausrechnen. Also wir hätten gern das First Set von S, das First Set von B und das First Set von C und dafür verwenden wir die Definition von der vorigen Folie, die wir hier nun so lang rekursiv immer wieder anwenden, bis sich nichts mehr ändert. Ein einfacher Weg hier anzufangen ist bei der letzten Regel oder bei den letzten Regeln, indem wir uns erstmal anschauen, was ist denn das First Set von C. C hat diese zwei Regeln, wo einmal vorne das kleine C steht, also ein Terminal und beim anderen Fall das kleine D, also auch ein Terminal. Das heißt, wir können direkt ausschreiben, dass D und C im First Set von Groß C sind. So ähnlich können wir das machen auch für B. Da sehen wir an der ersten Regel von B, dass das direkt nach dem Terminal Klein B abgeleitet werden kann. Also wissen wir, dass das B hier auf jeden Fall im First Set von Groß B ist. Wir sehen aber auch in der zweiten Regel von B, dass B nach C abgeleitet werden kann und das bedeutet, dass wir uns jetzt auch noch anschauen müssen, was im First Set von C ist, denn wenn wir ein B ableiten kann, eben alles, was als erstes in der Ableitung von C kommen kann, dann auch in der Ableitung von B kommen. Das heißt, wir nehmen quasi alles, was hier unten drin ist und schieben das auch da hoch ins First Set von B. Was bedeutet, dass wir konkret C und D hier auch noch eintragen? Ich lasse mal noch offen, weil theoretisch könnte dann natürlich überall noch mehr kommen. Jetzt schauen wir mal das First Set von S an. Wir wissen, dass S abgeleitet werden kann mit der ersten Regel, wo das Terminal A als erstes kommt. Deswegen haben wir das schon mal hier. Dann gibt es die zweite Regel von S, die sagt, S kann nach Groß B abgeleitet werden. Das ist alles, was in First von B ist, auch in First von S. Sprich, wir haben hier noch B, C und D drin. Und jetzt gibt es noch eine andere Form, wie wir das rekursiv wieder einsetzen können. Ach nee, gibt es doch nicht. Genau, jetzt können wir noch mal durchschauen, ob es irgendeine Art und Weise gibt, wie wir noch die First Sets, die wir aktualisiert haben, wieder einsetzen müssen, um die anderen First Sets zu erweitern. Das ist ja aber nicht der Fall, weil sich da nichts mehr ändern würde. Das heißt, wir können die Mengen hier alle zumachen und haben die Definition von der vorigen Folie häufig genug angewendet, um die First Sets für all die Nichtterminale in dem Beispiel ausgerechnet zu haben. So, schauen wir uns mal noch ein zweites Beispiel an. Für das Beispiel haben wir wieder eine Grammatik, die ist mal mit fünf Regeln. Die Stadtsinbol heißt P. Und das kann abgeleitet werden nach I oder C oder NTS, wobei T und S weitere Nichtterminale sind. Dann gibt es da ein Nichtterminal Q, was abgeleitet werden kann nach unserem Stadtsinbol P oder nach A, gefolgt von S oder nach D, S, C, ST. Das dritte Nichtterminal, das wir haben, ist R. R kann abgeleitet werden nach dem Terminal B oder nach dem Epsilon, also unserem leeren Watt. Das Epsilon ist hier interessant, weil wir dann auch mal den Fall der Definition sehen werden, wo das Epsilon eine Rolle spielt. Und dann müssen wir noch die beiden verbleibenden Nichtterminale definieren, die in der Grammatik schon verwendet werden, nämlich das S, was abgeleitet werden kann, oder nach R, N oder ebenfalls nach dem leeren Watt Epsilon und schlussendlich das T und das kann abgeleitet werden nach R, S und Q. Dann haben wir jetzt die Grammatik. Jetzt wollen wir die First Sets aller Nichtterminale ausrechnen. Sprich, wir möchten gern schlussendlich First von P haben. First von Q und First von R, First von S und schlussendlich auch First von T. So, jetzt fangen wir mit den einfachen Fällen an. Also wir schauen uns einfach alle Regeln an, die direkt nach einem Terminal ableitbar sind. Und da sehen wir zum Beispiel, dass P direkt zu I führen kann, zu C führen kann und zu N führen kann. Q kann zu A führen, wenn wir die zweite Regel für Q nehmen oder zu D. R kann zu B führen, S kann zu E führen und T hat keine Ableitung, die offensichtlich einen Terminal von R hat. Dann andere einfache Fälle sind die mit dem Epsilon. Wir sehen, dass R zum leeren Watt Epsilon abgeleitet werden kann. Also tragen wir das hier ein und das gleiche auch für S. Und jetzt gehen wir einfach unsere Regeln nochmal von vorn an durch. Bei P gibt es keine anderen Möglichkeiten, als diese drei Terminale, die wir da schon eingetragen haben. Sprich, First von P ist an der Stelle auch schon fertig. Q hat neben den zwei Fällen, die wir schon eingetragen haben, noch die Möglichkeit, dass nach P abgeleitet wird. Das heißt also, wir müssen alles, was in First von P ist, auch in First von Q eintragen. Also kommt hier i, c und n noch dazu. Bei R kann nichts anderes passieren, weil diese beiden Fälle, die es gibt, haben wir beide schon drin. S ist jetzt interessant, da haben wir diese beiden Fälle eigentlich schon drin, aber dann gibt es auch noch den Fall, dass S nach R abgeleitet wird. Das heißt, wir müssen alles, was in R ist, hier auch noch eintragen, also B und Epsilon, das Epsilon haben wir aber schon. Und weil das R selbst auch nach Epsilon abgeleitet werden kann, das ist jetzt dieser mittlere Fall in der Definition, müssen wir auch alles noch eintragen, was hinter dem R kommt. Sprich, wir tragen hier auch noch das N ein. So, und dann bleibt nur noch T übrig. T kann nach R abgeleitet werden. Das heißt, wir haben das B und das Epsilon hier schon drin. R selbst kann aber auch Epsilon werden. Sprich, wir müssen auch alles eintragen, was in S drin ist. Sprich, hier kommt noch das B dazu und das N. Und weil S selbst auch nach Epsilon abgeleitet werden kann, kann es sein, dass diese beiden nicht terminale hier vorne beide zu nichts aufgelöst werden. Und deswegen haben wir auch noch das Q hier drin, weil das dann anschließend kommt und dann das erste wäre, was aus dem T konkret abgeleitet wird. So, und damit sind wir auch fertig und haben unsere First Sets für alle nicht terminale der Grammatik ausgerechnet. So, jetzt habe ich das Ganze jetzt zweimal am Beispiel gezeigt und das Ganze jetzt auch noch zu festigen und sie in die Lage zu versetzen, First Sets selbst ausrechnen zu können. Würde ich Sie bitten, das mal selber an einem kleinen Beispiel zu machen, und zwar mit dieser Grammatik, die hier gezeigt ist, die hat vier Regeln, vier Nicht-Terminale. Das heißt, wir müssen vier First Sets nämlich für STQ und R berechnen. Machen Sie das mal und dann so als kleine Hash-Summe über das, was Sie herausbekommen, schauen Sie einfach mal an, wie groß diese vier Mengen dann jeweils sind und addieren diese vier Zahlen zusammen und mit dieser Zahl können Sie dann im Ilias abstimmen, um zu zeigen, dass Sie das Ganze hoffentlich auch gut verstanden haben. So, dann zeige ich Ihnen mal die Lösung für das Quiz. Also, wir wollten vier First Sets ausrechnen, nämlich für S und hier können wir gleich mal anfangen, das enthält A und ansonsten auch nichts weiter. Das ist das First Set für T. Enthält alles, was in First von R und Q und dann auch S drin sein wird. Also müssen wir erstmal die anderen ausrechnen. Fangen wir mal hier mit R an. Das enthält das kleine R und das Epsilon. Und hier unten das First Set von Q enthält ebenfalls das Epsilon und das First Set von S. Das First Set von S enthält A, also haben wir hier noch das A drin und das war es dann auch. So, und jetzt können wir das First Set von T ausrechnen. Das enthält alles, was in R drin ist, sprich das kleine R, alles, was in Q drin ist, denn das, weil Q ja auch einer der Möglichkeiten ist, nach den T abgeleitet werden kann, also haben wir hier das A und weil R nach Epsilon abgeleitet werden kann, haben wir auch alles, was in First von S ist, aber das ist das A, das haben wir schon und dann schlussendlich gibt es noch die Variante, genau, weil es nach Q abgeleitet werden kann, haben wir auch noch das Epsilon hier drin. So, das sollte es gewesen sein, dann zählen wir mal die ganzen Elemente zusammen, das sind 8, d.h. die Summe, die Sie in Ilias, wenn Sie es richtig gemacht haben, hoffentlich eingegeben haben, ist dann 8. So, was haben Sie gesehen, wie die First Sets berechnet werden, bloß, um nochmal ein bisschen Kontext zu geben, wir interessieren uns für diese First und jetzt auch die Follow Sets, weil wir mit diesen beiden dann die Predict Sets ausrechnen können und die helfen schlussendlich, den Pause zu entscheiden, welche von verschiedenen Optionen er daneben soll, dass es mehr als eine mögliche Regel gibt, die man als nächstes anwenden könnte. Schauen wir mal die Follow Sets an. Also, was das intuitiv bedeutet ist, ist, dass wir für jedes Nicht-Terminal sagen, Follow von diesem Nicht-Terminal sind all die Terminale, die genau nach diesem ausgefalteten Nicht-Terminal kommen können. Also, wir nehmen das A, expandieren das, soweit es geht und entscheiden, ist dann, welches Token kommt genau dahinter als nächstes, also welches folgt dem A sozusagen. Da gibt es zwei Spezialfälle, die wir auch betrachten müssen. Das eine ist, es kann ja sein, dass nach dem Ableiten eines Nicht-Terminals gar nichts mehr kommt, ganz einfach, weil das Programm da zu Ende ist und hier verwenden wir dieses spezielle Symbol End of File, was einfach sagt, da ist dann Ende und der andere Fall ist, es kann sein, dass danach eigentlich erst mal das Lehrrebot kommt, aber das Lehrrebot interessiert uns an der Stelle nicht. Das heißt, wir definieren einfach, dass die Followsets, egal von welchem Nicht-Terminal, niemals das Epsilon enthalten. Bevor wir jetzt die Definition anschauen, schauen wir mal ein kleines Beispiel an, nämlich diese Grammatik hier unten, wo wir einfach zwei Regeln haben. S ist das Startsymbol und B ist ein anderes Nicht-Terminal. Für S greift eben genau diese eine Sonderfall, den ich gerade erklärt habe, weil S das Startsymbol ist und nach dem Startsymbol, wenn das vollständig expandiert wurde, nichts mehr kommt, ist im Followset von S eben genau dieses Spezialzeichen End of File und für B ist die Frage, welches Token kann nach dem B kommen, wenn B abgeleitet wurde, B wird hier oben verwendet und das, was dann anschließend kommt, ist eben genau dieses C und dann haben wir im Followset von B dann das kleine C drin. Nach diesem Beispiel schauen wir uns mal den allgemeineren Fall an und was Sie jetzt hier sehen, ist im Prinzip die Definition oder der Algorithmus, wie man die Followsets ausrechnet. Wir wollen hier Follow von A berechnen und da gibt es drei Gruppen von Fällen. Der Einfachste ist, wenn A das Startsymbol ist, dann müssen wir ganz einfach dieses Spezialsymbol End of File in Follow von A einfügen, wenn wir das Startsymbol erfolgreich expandiert haben, sind wir am Ende der Datei bzw. am Ende des Programmes, das wir hier pausen wollen. Wenn wir eine Regel haben, in der A auftritt, sodass nach dem A noch etwas anderes kommt, was wir hier einfach mal als Beta bezeichnen, dann fügen wir Folgendes zu Follow von A hinzu, nämlich alles, was als erstes in Beta kommt. Das heißt, wir benutzen hier die hoffentlich schon ausgerechneten Firstsets, in denen wir sagen, alles, was als erstes in diesem Beta auftritt, also First von Beta, aber nicht das Epsilon, denn wir hatten gesagt, Epsilon ignorieren wir für die Followsets, das fügen wir dann alles zu Follow von A hinzu. Der dritte Fall ist der hier unten und zwar geht es darum, den Fall, dass A in eine Regel auftritt, allerdings am Ende oder gefolgt von einem Beta, was aber dann selbst zum leeren Wort abgeleitet werden kann, also sprich, das A ist quasi auch in dem zweiten Fall am Ende. Und was wir hier machen müssen, ist, dass alles, was nach B kommen kann, ja dann schlussendlich auch nach A kommen kann. Das heißt, wir fügen alles, was wir in Follow von B haben, unsere Menge Follow von A hinzu. Diese ganzen Regeln werden jetzt wieder so lange angewandt, bis sich nichts mehr ändert, also ganz ähnlich wie auch schon bei den Firstsets. So, schauen wir mal ein Beispiel dazu an. Für das Beispiel brauchen wir wie immer eine Grammatik, die sieht in dem Fall so aus. Wir haben S als unser Startsymbol, das kann abgeleitet werden, nach A, oops, das soll ein kleines E werden, A S E oder großes B. Das große B wird abgeleitet nach dem kleinen B, großen B, C und kleinem F oder dem großen C. Und das große C wird dann abgeleitet nach kleinem C, großen C, G oder D oder dem Epsilon. Und um die Followsets für S und B und C jetzt ausrechnen zu können, brauchen wir zunächst erstmal die Firstsets, die üblicherweise vorher berechnet werden. Ich schreibe die jetzt hier einfach mal hin und Sie haben ja nun schon genug Beispiele gesehen, dass Sie das selber auch nochmal ausrechnen können. Also wer möchte können das jetzt auch einfach nochmal selber durchrechnen zum Test. Also für S wäre das A, B, C, D und Epsilon. First von B enthält dann B, C, D und Epsilon. Und First von C enthält C, D und ebenfalls wieder das Epsilon. So, jetzt haben wir das alles gegeben. Was wir jetzt machen wollen, ist die Followsets, diese drei, nicht terminale auszurechnen. Also wir wollen gern Follow von S ausrechnen und Follow von B und schlussendlich ebenfalls Follow von C. Fangen wir mal mit dem S an. Also da es das Start-Symbol ist, ist da auf jeden Fall dieses Spezial-Symbol End of File drin. Und dann außerdem sehen wir das S hier auch nochmal hier vorkommt, wo es dann gefolgt wird von dem Terminal E. Das heißt, wir haben das E auch in Follow von S. Und damit haben wir eigentlich alle Stellen, wo es vorkommt, abgedeckt, sprich wir können das Followset an der Stelle auch schon schließen. So, dann schauen wir uns mal an, was in Follow von B alles rein muss. Also das B tritt einmal hier auf, wo es vom C gefolgt wird. Da C ein nicht Terminal ist, müssen wir uns anschauen, was in First von C ist. First von C ist hier unten und das enthält C und D und Epsilon. Das Epsilon nehmen wir aber nicht, weil das in der Definition ja explizit ausgeschlossen wurde. Dann kann es sein, dass dieses C hier, weil First von C ja Epsilon enthält, nach Epsilon abgeleitet wird, also nichts ist, sprich das nächste, was nach dem B kommen könnte, wäre dann dieses F hier drüben. Also es ist in Follow von B auch noch das F drin. Und dann taucht B auch noch hier oben auf. Das heißt, alles, was nach dem S kommen kann, kann auch nach dem B kommen. Also kopieren wir die zwei Sachen, die in Follow von S oben sind, nämlich das Enter-File und das E auch hierunter in das Follow von B. Und damit haben wir eigentlich alle Stellen, an denen B auftreten kann, abgearbeitet und können die Menge auch schon schließen. So, dann bleibt jetzt noch Follow von C. Da müssen wir also alle Stellen anschauen, an den C auftritt. Das wäre einmal hier oben, wo es von F gefolgt wird. Dann tritt C hier auf, am Ende und zwar an der Regel B. Das heißt, alles, was in Follow von B ist, müssen wir auch in Follow von C aufnehmen. Also können wir einfach mal alles runterschreiben. Das F ist schon drin und dann Enter-File und E. Dann tritt C noch hier auf, wo es vom G gefolgt wird. Also haben wir G auch noch drin. Und das müsste es eigentlich auch schon gewesen sein. Ich schau gerade nochmal durch. Das waren alle Stellen, an denen C auftritt. Sprich, diese Menge können wir auch zu machen und wissen jetzt also unsere Follow-Sets für alle Nichtterminale für S, B und C. So, jetzt habe ich das ja einmal vorgerechnet. Als nächstes hätte ich ein kleines Quiz für Sie, wo Sie einfach mal selbst die Follow-Sets ausrechnen sollen und zwar für die Grammatik, die Sie hier sehen. Um die Follow-Sets für S, T, R und Q auszurechnen, brauchen Sie natürlich die First-Sets. Wer genau hinschaut sieht, dass das genau die sind, die vorhin im ersten Quiz auch schon abgefragt waren. Sprich, das müssen Sie nicht noch mal machen, sondern Sie sollen mithilfe dieser First-Sets, die Sie ja schon haben, jetzt die Follow-Sets für die Nichtterminale hier ausrechnen und dann wieder zählen, wie groß diese vier Mengen sind, diese vier Zahlen einfach zusammenaddieren. Und das ist dann so die Hash-Summe, mit der Sie bitte in Ilias abstimmen, um zu checken oder zu zeigen, dass Sie das hier tatsächlich verstanden haben. So, schauen wir uns mal die Lösung dazu an. Also, was wir hier machen wollen, ist für diese vier Nichtterminale S und T und R und Q, die First-Sets und Follow-Sets auszurechnen. First hatten wir vorhin schon gemacht. Die schreibe ich jetzt einfach nochmal kurz hin. Das war A hier und dann R, A und Epsilon hier, R und Epsilon hier und schlussendlich A und Epsilon hier. Und was wir jetzt machen wollen, ist die Follow-Sets dazu ausrechnen und fangen wir mal mit S an. S ist das Start-Symbol, das heißt, end of file ist definitiv in dem Follow-Set drin. Jetzt müssen wir schauen, wo das S alles noch auftritt. In der ersten Ableitung, die S ermöglicht, bis nach dem S direkt das kleine E, also ist das hier auch dabei. In der zweiten Ableitung tritt nach dem S das T auf. Das Ganze passiert auch unten bei der Regel von Q. Das heißt, wir müssen alles, was in First von T ist, auch in das Follow von S einfügen, außerdem Epsilon selbst. Und in First von T haben wir zusätzlich hier noch R und A. Dann gibt es noch ein paar andere Stellen, an denen S auftritt, aber da kommen dieselben Tokens auch nur wieder rein und damit ist das Follow-Set an der Stelle von S auch schon vollständig. Dann schauen wir mal die Follow-Sets von T, R und Q noch an. T tritt auf in der zweiten Option der Regel von S. Wo es von S gefolgt wird, also alles, was in First von S ist, muss auch in Follow von T sein. In First von S haben wir das A und das war es erstmal. Dann schauen wir uns das R an. Das R kann auch von S gefolgt werden, zum Beispiel in der ersten Regel vom T. Das heißt, wir haben auch wieder First von S hier drin, also auch wieder das A. Schauen wir uns Q an. Wo ist denn das Q? Das Q tritt auf am Ende der zweiten Regel von T. Das heißt, alles, was in First in Follow von T, es muss auch in Follow von Q sein, sprich, wir haben hier noch das A. Und dann gibt es noch so ein paar Stellen, wo wir Dinge wieder ineinander einsetzen, aber da das alles nur ein Mengen mit A sind, ändert sich daran nichts und dann sind wir an der Stelle auch schon fertig. So, jetzt nochmal die Check Summe. Das sind also 4 plus 3 insgesamt 7 Elemente hier in diesen Mengen. Das heißt, die richtige Antwort im Ilias wäre dann 7 gewesen. So, jetzt wissen Sie, wie man Firstsets berechnet. Sie wissen, wie man Followsets berechnet. Jetzt können wir das Ganze endlich zusammenführen und mithilfe dieser beiden Mengen der Firstsets und Followsets die Predict-Sets für bestimmte Regeln unserer Kramatik ausrechnen. Der Grund, warum wir das machen wollen, ist, dass das dem PASA hilft, zu entscheiden, welche von verschiedenen möglichen Ableitungsregeln er denn anbinden soll. Also insbesondere wird das Ganze in LL1-Pasen verwendet, also wo der PASA ein Token vorausschaut. Und was der PASA hier machen möchte, ist, er möchte schauen, welcher Token ist denn da und je nachdem, welcher Token da ist, wird dann diese Regel oder jene Regel angewandt. Wenn wir eben diese Vorhersage funktioniert jetzt über unsere Predict-Sets, und zwar so, dass wenn ein Token im Predict-Set einer bestimmten Regel ist, dann nehmen wir diese Regel. So, wie berechnen wir jetzt diese Predict-Sets? Also wenn wir eine Regel haben, die A nach Alpha ableitet, wobei Alpha wieder eine Verkettung von Terminalen und Non-Terminalen ist, dann gibt es zwei Fälle. Der einfache Fall ist der hier unten. Dem schauen wir uns einfach an, was ist denn im First von Alpha? Also was kann als Allererstes kommen, wenn wir diese Regel anwenden? Und diese Tokens, die als Allererstes kommen, sind ganz einfach die Tokens, die im Predict-Set für diese Regel A geht nach Alpha sind. Der etwas kompliziertere Fall ist, wenn im First von A selbst das Epsilon drin ist, also sprich, wenn A nach dem leeren Watt abgeleitet werden kann und sozusagen verschwindet, denn in dem Fall müssen wir nicht nur das betrachten, was aus Alpha selbst rauskommen kann, sondern eben auch den Epsilon-Fall betrachten und schauen, was kann denn nach dem A kommen? Also das A wird nach dem leeren Watt abgeleitet und deshalb interessieren wir uns dafür, was danach kommt. Und das sagt genau diese Gleichung hier unten aus. Die sagt, das Predict-Set von A geht nach Alpha, enthält alles, was in First von Alpha ist, außerdem Epsilon-Falls, das da drin ist und außerdem alles, was dem A noch folgen kann, also das, was wir in Follow von A schon berechnet haben. Und mit diesen beiden Fällen hier unten bekommen wir sozusagen dann das Predict-Set für jede Regel innerhalb unserer Grammatik. Schauen wir uns das Ganze mal mit einem Beispiel an, und zwar wieder mit der Grammatik, die wir in der letzten Einheit schon gesehen haben, als Beispiel für den Recursive-Descent-Pase, das ist die hier oben mit den vier Regeln, die beschreibt, was nach was S, B und C denn so alles ableitbar sind. Sie wissen jetzt, wie man First und Follow-Sets berechnet. Das Ganze ergibt dann das, was hier unten dargestellt ist, also das First-Set von S zum Beispiel enthält A und B oder das Follow-Set von C enthält End of File. Mit diesen Definitionen können wir jetzt die Predict-Sets ausrechnen, so wie auf der letzten Folie beschrieben, und zwar nicht pro Nicht-Terminal, sondern pro Regel in der Grammatik. Das heißt, wir haben nicht nur drei, sondern vier Predict-Sets hier, und das ist, was man hier sieht, also das Predict-Set der ersten Regel für S enthält A. Was bedeutet, wenn ich ein A sehe, als den nächsten Token, den der Paser sich schon anschaut, dann wissen wir, dass wir diese erste Regel hier anwenden müssen. Das Ganze kann dann in verschiedenen Weisen implementiert werden, und eine Implementierung ist eben genau das, was wir in dem Recursive-Descent-Paser in der letzten Einheit schon gesehen haben, wo wir eben genau diese Predict-Sets benutzen, um festzulegen, welche der Regeln, die für einen Nicht-Terminal existieren, wir denn benutzen wollen. Also nochmal das Beispiel von dem A für die erste Regel. Hier im Code schauen wir dann also, dass das nächste Input Token gleich dem A ist. Und wenn ja, dann führen wir das entsprechende Match von dem A und dieslich den Aufruf von B, um den zweiten Teil des Inputs hier zu lesen aus. Falls aber der Input ein B sein sollte, dann nehmen wir eben nicht die erste Regel, sondern die zweite und führen dann die entsprechenden Match- und Funktionsaufrufe auf, um das zu implementieren. Und relativ zu der Implementierung, die wir da gerade jetzt gesehen haben, also dieser Recursive-Descent-Paser-Implementierung, kann man das Ganze auch wieder Tabellen basiert machen. Also so ähnlich wie das, was wir schon bei Scanners gesehen haben, gibt es dann ein generisches Programm, was alle möglichen Paser implementiert, was aber eine Tabelle benötigt, die beschreibt, welche Schritte für die gegebenen Grammatik ausgeführt werden sollen. Und solch eine LL1-Tabelle baut man dann wie folgt, dass diese LL1-Tabelle schlussendlich macht, ist ein Mapping M zu definieren, dass jedem Nicht-Terminal kombiniert mit einem Terminal, entweder eine Produktionsregel, die benutzt werden soll, zuordnet, oder sagt, hey, das geht nicht. Und in dem Fall wird es dann ein Fehler ausgegeben, weil das nächste Talken so nicht mehr entsprechend der Grammatik verarbeitet werden kann. Konkret funktioniert das so, als die Tabelle wird so gefüllt, dass durch alle unsere Produktionsregeln A geht nach Alpha einmal durchgehen. Schauen, was im Predict Set dieser Produktionsregel drin ist. Das ist eine Menge von Terminalen. Und für jedes dieser Terminale T schreiben wir dann in unsere Tabelle, also in das Mapping, rein, dass wenn wir das Nicht-Terminal A sehen und das Talken T dazu sehen, dann wird eben genau diese Regel A geht nach Alpha ausgeführt oder ausgewählt. Und falls für alles andere, für alle anderen Kombinationen von Nicht-Terminalen und Terminalen gibt es keinen Eintrag in der Tabelle und das bedeutet ganz einfach, dass eben ein Fehler auftritt. Schauen wir uns mal ein Beispiel dafür an. Und zwar für die kleine Grammatik, die ich gerade eben schon gezeigt habe, und den entsprechenden Predict Sets dazu, die wir hier auch noch mal sehen. Die Tabelle dafür würde wie Volks aussehen. Also machen wir erstmal die grobe Struktur, was die Tabelle hat, sind zwei Dimensionen, nämlich einmal die Nicht-Terminale und dann hier die Terminale, die Nicht-Terminale in dieser Grammatik sind S, B und C. Und die Terminale, die wir hier haben, sind A, B und C. Die Predict Sets sagen uns jetzt genau, in welchem Fall nun welche Regel benutzt werden soll. Ich nummeriere die Regeln einfach mal durch, von 1 bis 4, von oben nach unten. Und was die Predict Sets uns hier sagen, ist, wenn wir das Terminal A sehen und das Nicht-Terminal S expandieren wollen, dann müssen wir Regel 1 benutzen. Falls wir S expandieren wollen, aber Terminal B sehen, müssen wir Regel 2 benutzen und Analog für B und das kleine B, in welchem Fall wir Regel 3 benutzen und beim großen C und dem kleinen C, wird das Regel 4 sein. Und das sind auch alle Fälle, die es gibt. Das heißt, alle sonstigen Fälle sind nicht definiert. Und falls wir an die Stelle kommen sollten, während des Parsens, bedeutet das ganz einfach, dass ein Fehler auftritt. So, mithilfe einer solchen Tabelle können wir jetzt einen generischen Parsing-Algorithmus definieren, der die Tabelle nutzt um den gegebenen Token Stream in einen Passbaum zu übersetzen. Und das ist genau, was wir hier auf der Folie sehen. Also, was dieser Algorithmus macht, ist, ein Token nach dem nächsten einlesen und den Passbaum Top-Down aufzubauen. Und zwar gibt es eine Reihe von interessanten Variablen. Das eine ist dieser Stack, was der Stack, auch Pass-Stack genannt, repräsentiert. Ist alles, was wir in der Zukunft noch erwarten zu sehen. Also insbesondere pushen wir da ganz am Anfang End-of-File drauf, weil wir erwarten, dass irgendwann der Code, den wir einlesen, zu Ende ist und als letztes Token End-of-File kommt. Anschließend pushen wir das Start-Symbol der Grammatik drauf, denn wir erwarten oder gehen davon aus, dass es natürlich immer mit dem Start-Symbol losgeht. Während der Algorithmus ausführt, wird gelegentlich dann was von dem Stack runtergenommen und manchmal noch wieder was drauf getan und wie genau das sehen wir gleich. Das Ganze passiert innerhalb dieser großen Schleife hier, in der ein Token nach dem anderen eingelesen wird, also diese Look-Ahead-Funktion, liest den nächsten Token, speichert ihn dann nach Next-Token und das Ganze geht so lange, bis wir wieder bei dem End-of-File angekommen sind, was wir ganz am Anfang auf den Stack gepusht haben. In der Schleife poppen wir jeweils ein Element von unserem Stack runter und je nachdem, was das dann ist, gibt es zwei Möglichkeiten. Wenn dieses Element, was ganz oben auf dem Stack lag, ein Terminal ist oder End-of-File, dann schauen wir, ob dieses Terminal tatsächlich der nächste Token ist, den wir gelesen haben, also ob die Erwartung, die wir hatten, tatsächlich eintrifft. Wenn das so ist, dann haben wir quasi ein Token konsumiert und können das nächste Token einlesen, indem wir wieder Look-Ahead aufrufen. Der zweite Fall ist, dass das, was wir vom Stack runtergenommen haben, ein Nicht-Terminal ist und hier schauen wir jetzt in unsere Tabelle rein und schauen nach, was wir, wenn wir dieses Nicht-Terminal haben und einen bestimmten nächsten Token schon sehen, für ein Regel anwenden sollen und das verrät uns hoffentlich unsere Tabelle und sagt uns dann, dass wir dieses X ableiten sollen nach irgendwelchen Symbolen, Y1, Y2 bis Ym und diese pushen wir jetzt auf den Stack und zwar in der umgedrehten Reihenfolge, sodass wir als erstes dann Y1 wieder runter poppen anschließend. Falls in der Tabelle nix drinstehen sollte, dann ist genau dieser Fehlerfall eingetreten und wir wissen, dass das Paarse an der Stelle nicht geklappt hat und der Paarse würde einen Fehler zurückgeben und sagen, diese Token-Sequenz ist nicht legal entsprechend der Kramatik. Schauen wir uns das Ganze mal wieder an einem Beispiel an. Also das wird ein Beispiel für das Table-Based-Parsing. Und zwar nutzen wir wieder die Kramatik, die wir gerade eben schon gesehen haben und brauchen jetzt natürlich auch einen konkreten Input-String, der gepasst werden soll und dann nehmen wir mal BCC. So, was ich jetzt machen werde, ist den Algorithmus, wie der ausgeführt wird, Schritt für Schritt zu erklären, und zwar indem ich hier so eine kleine Tabelle aufmalle, in der wir für jeden Schritt den aktuellen Paar-Stack sehen, den noch einzulesenden Input, also die Tokens, die noch nicht eingelesen wurden, und dann schreibe ich jeweils noch die Schritte auf, die konkret ausgeführt werden. So, das erste, was der Algorithmus macht, ist End of File und das Start-Symbol der Kramatik auf den Stack zu pushen. Das heißt, wir haben hier dann End of File und das S. Zunächst ist der verbleibende Input für alle Tokens, alles was wir haben, also alle Tokens, sprich BCC und der spezielle Token für End of File. Und was wir jetzt machen, ist, wir schauen uns das oberste Stack-Element an, nehmen das runter, das ist das große S und schauen mithilfe dieses Nicht-Terminals, das wir gerade runtergenommen haben und das nächsten Tokens, dem B in der Tabelle nach, um herauszufinden, welche Regel wir jetzt anwenden sollen. Gehen wir zurück zur Tabelle, also großes S, kleines B, da kommen wir genau hier raus und die Tabelle sagt uns, dass wir das zweite Element oder die zweite Regel benutzen sollen. Die zweite Regel ist die, die sagt, dass S abgeleitet werden kann nach kleinem B, großen C. Und was wir damit jetzt machen, ist, wir nehmen alle Symbole, die auf der rechten Seite dieser Regel sind, die in umgekehrter Reihenfolge auf den Stack, also zunächst das C und dann das B. Anschließend sieht unser Stack dann also so aus, wir haben das große S runtergenommen, dafür aber das große C drauf gepusht und das kleine B und unser Input sieht noch so aus wie vorher, weil wir noch nicht das nächste Token eingelesen haben. So, das schreibe ich mal ein bisschen weiter runter, damit wir hier auch auf der richtigen Zeile rauskommen. Gut. Jetzt poppen wir wieder das nächste Element vom Stack. Das ist jetzt das kleine B und weil es ein Terminal ist, schauen wir nach, ob dieses kleine B dem nächsten Token in der Inputsequenz entspricht. Das tut es, das heißt, wir sind auf dem richtigen Weg und können also den nächsten Token einlesen und unser Zustand dann so aus, dass das B weg ist, weil das haben wir ja gepoppt. Das B beim Remaining Input ist auch weg, stattdessen sind nur noch CC und End of File übrig. So, jetzt kommt der nächste Schritt. Wir poppen wieder das oberste Element vom Stack. Das ist jetzt das große C. Schauen wir wieder nach, welche Regel wir jetzt anwenden sollen, also großes C, kleines C, bedeutet, die vierte Regel wird angewandt und die vierte Regel ist die, die uns sagt, dass das große C nach zweimal klein C abgeleitet werden kann und das bedeutet, wir pushen alles, was auf der rechten Seite dieser Regel ist, also die zwei kleinen Cs auf den Stack, der anschließend dann so aussieht, dass wir End of File die beiden kleinen Cs haben. Der Input sieht aus wie gehabt, weil wir noch keinen weiteren Token gelesen haben. Jetzt poppen wir wieder das oberste Element vom Stack, das kleine C, das ist wieder ein Terminal, wir schauen, ob es das Gleiche ist, wie das, was ganz vorne in unserem Input ist. Also der nächste Token, das ist das Gleiche. Wunderbar, also können wir den nächsten Token einlesen. Unser Stack hat jetzt noch 1C drauf und es gibt auch noch genau 1C einzulesen, bevor wir dann zum End of File kommen. Und jetzt geht es im Prinzip einfach runter so weiter, bis alles vom Stack abgeräumt wurde. Wir poppen wieder das kleine C, sehen, dass das dasselbe ist wie der nächste Token, können also auch diesen Token erfolgreich einlesen. Und was da noch übrig bleibt ist nur das End of File und das End of File ebenfalls in der Token Sequenz. Das poppen wir und an der Stelle ist der Algorithmus dann auch schon fertig und hat erfolgreich diesen Input BCC eingelesen und dabei den Pass Tree aufgebaut und gezeigt, dass dieser Input legal entsprechend unsere Grammatik ist. Ja, damit werden wir auch am Ende dieser Einheit. Wir haben jetzt gelernt, wie Top Down Parsing funktioniert und insbesondere gesehen, wie man ein Top Down Paser, genauer gesagt einen LL1 Paser automatisch aus einer Grammatik generieren kann, indem man zunächst diese First and Follow Sets berechnet, daraus dann die Predict Sets und mithilfe derer dann entweder ein Recursive Descent Paser aufschreibt oder wie gerade eben gesehen ein Tabellen basierten Paser. Das war jetzt die fünfte von 6 Einheiten und das Einzige, was jetzt noch verbleibt und was wir uns in der nächsten Einheit anschauen werden, ist, wie man nicht Top Down, sondern bottom up passt und das machen wir dann beim nächsten Mal. Bis dahin, danke fürs Zuhören und bis dann.