Compiler für unsere eigene Programmiersprache Eigene Programmiersprache Im letzten Praktikum haben Sie einen Scanner und einen Parser programmiert, welche arithmetische Ausdrücke mit natürlichen Zahlen auswerten und daraus Bytecode erzeugen konnte. Jetzt wollen wir einen Schritt weiter gehen und den Parser so ergänzen, dass er unsere eigens kreierte Programmiersprache verstehen kann. Diese Programmiersprache soll neben der Auswertung von arithmetischen Ausdrücken auch Zuweisungen und Sprünge (unbedingte und bedingte) verarbeiten können. Die Programmiersprache soll konkret folgender Grammatik entsprechen: expr = term {("+" | "-”) term} term = factor {("*" | "/") factor} factor = number | ident | "(" expr ")" assignement = ident "=" expr statement = [number] (assignment | condition | "goto" number) condition = "if" ["not"] expr "goto" number program = statement {statement} Assignement: einer lokalen Variablen wird ein Ausdruck zugewiesen, z.B. a = 6+2. Zusätzlich sollen in Ausdrücken (expr) ebenfalls Variablen zugelassen werden, z.B. i =i -1 Statement: eine Anweisung, die optional mit einem Label (als Nummer) beschriftet ist. Über diese Nummer kann mit goto auf diese gesprungen werden, z.B. 10 a = 6 + i oder goto 10. Condition: überprüft einen Wert auf 0 oder ungleich 0. Wird die Bedingung erfüllt (Wert != 0), wird mit goto number zur gewünschten Anweisung gesprungen, z.B. if i goto 10 Program: besteht aus einer oder mehreren Anweisungen. Aufgabe 1 Als erstes sollen Sie den Parser so erweitern, dass auch lokale Variabeln gespeichert und verarbeitet werden können. Für diesen Zweck müssen Sie factor entsprechend erweitern und die Zuweisung implementieren. Eine solche Zuweisung besteht grundsätzlich aus einem Variabelnamen (der ein Speicherplatz zugewiesen wurde) und einem Ausdruck. Damit der Parser weiss, welcher Speicherplatz für welche Variabel reserviert wurde, wird eine Liste (locals) geführt. Beispiel-Programm x = arg0 a = 1 b = 2 c = 3 return = a*x*x + b*x + c Die Parameter seinen in den vordefinierten Variablen arg0, arg1, … und der Rückgabe Wert in return gespeichert (werden von codeGen automatisch definiert). Hinweis: Die Methode local liefert die Nummer des Slots der benannten lokalen Variablen oder erzeugt bei Bedarf eine neue Slot-Nummer; diese können für die entsprechenden Load und Store Operation verwendet werden. Aufgabe 2 Die neue Programmiersprache soll auch Bedingungen verarbeiten können. Das Konstrukt einer Bedingung sieht folgendermassen aus: "if" ["not"] expr "goto" number • "if" soll prüfen, ob ein Ausdruck ungleich 0 ist und wenn dies zutrifft mit goto zu einem durch number bestimmten Label im Programm springen (ähnliche wie C). Für die Auswertung soll der Ausdruck vor dem 0-Test in eine Ganzzahl umgewandelt werden (D2I Anweisung). • "if not" soll prüfen, ob ein Ausdruck gleich 0 ist und wenn dies zutrifft mit goto zu einem durch number bestimmten Punkt im Programm springen Ergänzen Sie als erstes die Methode condition(), damit die beiden erwähnten Fälle verarbeitet werden können. Auf die Kondition folgt ein goto number. Die number zeigt auf eine bestimmte Stelle im Code. Nun kann es sein, dass die number auf eine Passage im Code zeigt, die erst folgt d.h. sie wissen im Moment der Codeerzeugung gar noch nicht, wo sich diese Passage befindet bzw. wohin Sie springen müssen. Es hat sich als praktisch erwiesen, zuerst in allen Fällen einmal Code zu erzeugen, der noch ungültige (=null) Zieladressen (targets) enthält. Die später zu korrigierenden Instruktionen (Oberklasse: BranchInstruktion) werden sich in einer Hashtable gotos gemerkt und in einer fixup-Methode ergänzt - in der nächsten Aufgabe. Beispiel-Code – output = Summe von 1 bis 10 m = 10 return = 0 10 return = return + m m = m - 1 if m goto 10 Aufgabe 3 Ihr Parser ist jetzt im Stand Bytecode zu erzeugen. Damit der Parser wirklich funktionieren kann, müssen Sie noch das am Schluss von Aufgabe 2 erwähnte "Goto-Problem" lösen. Ein Statement kann optional mit einer Nummer beschriftet werden. statement = [number] (assignment | "goto" number) Mit goto number kann dann zu dieser Code-Passage gesprungen werden. Der Parser generiert eine Hashtable (labels), welche alle mit einem Label versehenen Statements speichert. In Aufgabe 2 haben wir alle goto’s (BranchInstruction) zwischengespeichert jedoch noch keine Ziele angegeben. Da jetzt der ganze Code geparst und erstellt wurde, können jetzt alle richtigen Zieladressen zugewiesen werden. Ergänzen Sie die Methode fixup() damit jede Sprunganweisung eine korrekte Zieladresse bekommt. Hinweis: • Bei jedem il.append wird ein InstructionHandle zurückgegeben, der sich für die Zieladresse gemerkt werden kann. Jedes Label definiert ein mögliches Ziel einer Sprungstelle. • setTarget der Klasse BranchInstruction kann verwendet werden, um die Adresse nachträglich zu setzen. Aufgabe 4 Nun sollten Sie einen funktionstüchtigen Parser besitzen, welcher die Syntax der neuen Programmiersprache in Bytecode übersetzten kann. Schreiben sie zur Kontrolle ein Programm, das die Fakultät einer Zahl berechnet.