Zwischencode-Generierung

Teil 1: Grundlagen

Dies sind nur die Grundlagen für die Erzeugung von Zwischencode. Man kann diesen Teil überspringen, allerdings wird man dann nichts verstehen. In Teil 2 wird die eigentliche Aufgabe, die Definition der Logik des Compilers, erläutert.

Codes und Ausführungszeitpunkte

Für das Verständnis müssen wir drei verschiedene Codes bzw. Sprachen unterscheiden:

Weiterhin unterscheiden wir zwei Ausführungszeitpunkte:

Das erste Licht im Dunkeln kann mit Bedingungen und Schleifen entzündet werden.

Bedingungen im Zwischencode

Betrachten wir folgenden Pascal-Code, den wir in Zwischencode überführen wollen:

        if variable1 < 5 and variable2 == 10 then
            variable1 = 10
    

Der Zwischencode unterstützt keine boolschen Operatoren mehr, wie hier and. Sie müssen daher durch mehrere verschachtelte Bedingungen abgebildet werden. Der Zwischencode unterstützt aber auch keine wirkliche Verschachtelung. Diese wird daher mit goto realisiert, durch die an beliebige Stellen im Zwischencode gesprungen werden kann.

Wir verwenden einen Bottom-up-Parser. Daher werden durch den Compiler die kleinsten Einheiten von links nach rechts zuerst verarbeitet. Vollziehen wir dies Schritt für Schritt nach, wird beim oben gegebenen Pascal-Code zuerst der Teil variable1 < 5, eine sogenannte Expression, verarbeitet.

Expressions sind Ausdrücke mit einem Wahrheitswert, dazu gehören Vergleichsoperatoren und boolsche Operatoren. Expressions können beliebig geschachtelt werden. Sie können schlussendlich als Bedingung in einem Statement, etwa einem if-Block oder einer Schleife, eingesetzt werden.

Für diese Expression soll der Compiler folgenden Zwischencode erzeugen:

        if variable1 < 5 goto ...    
        goto ...
    

Der Platzhalter ... bedeutet dabei, dass zu diesem Schritt der Compilezeit noch nicht bekannt ist, an welche Stelle das Programm zur Laufzeit hinspringen soll, wenn es das entsprechende goto erreicht. Anstelle der Platzhalter muss zu einem späteren Schritt während der Compilezeit daher noch jeweils eine Adresse (hier Zeilennummer) eingetragen werden, zu der dann zur Laufzeit durch Ausführung des goto gesprungen werden kann.

Der bisher erzeugte Zwischencode bewirkt also zur Laufzeit folgendes: Wenn die Expression variable1 < 5 wahr ist, springe an irgendeine (hier noch unbekannte) Stelle. Dadurch wird das goto in Zeile 2 übersprungen und nicht ausgeführt. Wenn aber die Expression in Zeile 1 falsch ist, wird das goto in dieser Zeile nicht ausgeführt und es wird der Reihe nach mit der nächsten Zeile fortgefahren. In Zeile 2 wird dann an irgendeine andere Stelle gesprungen.

Es handelt sich also um ein einfaches if-else, durch das an zwei verschiedene Stellen gesprungen werden kann, je nach dem, ob die Bedingung wahr (Gutfall) oder falsch (Schlechtfall) ist.

Setzen wir die Ausführung des Compilers fort, der dann die nächsten Zeilen Zwischencode generieren soll. Es wird einfach ein weiterer if-goto-Block für die zweite Expression hinzugefügt:

        if variable1 < 5 goto ...    
        goto ...
        if variable2 == 10 goto ...    
        goto ...
    

Nun wird der Compiler im Pascal-Code das umschließende and, das die beiden Expressions verbindet, erreichen. Bei diesem Schritt können einige Platzhalter, die noch im Zwischencode vorhanden sind, ausgefüllt werden, weil nun bekannt ist, wo das Programm zur Laufzeit hinspringen soll.

Bei der boolschen and-Operation gilt: Ist die erste Expression wahr, muss die zweite Expression ebenso überprüft werden. Ist die erste Expression aber falsch, braucht die zweite gar nicht mehr überprüft zu werden, da die and-Expression in keinem Fall mehr wahr werden kann. Es kann daher direkt in den else-Fall bzw. ans Ende des if-Blocks gesprungen werden.

        if variable1 < 5 goto 3    
        goto ...
        if variable2 == 10 goto ...    
        goto ...
    

Tatsächlich können wir leider erst einen Platzhalter ausfüllen: Wir wissen, dass zur Laufzeit, wenn die Expression in Zeile 1 wahr wird, zur Überprüfung der zweiten Expression in Zeile 3 gesprungen werden muss. In Zeile 2 muss an das Ende des if-Blocks gesprungen werden, es ist aber noch nicht bekannt, in welcher Zeile dieses Ende ist, da die entsprechenden Zeilen (zur Compilezeit) noch nicht erzeugt wurden. Dies gilt ebenso für Zeile 4.

Das goto in Zeile 3 soll als einziges zum Körper des if-Blocks springen, da nur dann gewährleistet ist, dass beide Expressions wahr sind (die erste Expression wurde ja bereits vorher überprüft). Auch die Position des Körpers des if-Blocks ist aber noch nicht bekannt. Es könnten ja theoretisch auch noch weitere Bedingungen hinzukommen, die noch nicht eingelesen wurden.

Setzen wir das Kompilieren fort. Als nächstes wird der Körper des if-Blocks erzeugt:

        if variable1 < 5 goto 3    
        goto ...
        if variable2 == 10 goto ...    
        goto ...
        variable1 = 10
    

In diesem Beispiel wird der Pascal-Code eins zu eins in den Zwischencode übernommen. Die Details interessieren uns hier nicht weiter. Im nächsten Schritt erreicht der Bottom-up-Parser das Ende und schließt somit den gesamten if-Block ab. Dabei kommen keine neuen Codezeilen mehr hinzu – die if-Statements im Zwischencode wurden ja bereits vorher bei den Expressions generiert. Es kann aber noch ein Platzhalter aufgelöst werden:

        if variable1 < 5 goto 3    
        goto ...
        if variable2 == 10 goto 5    
        goto ...
        variable1 = 10
    

Es ist nun bekannt, wo der Körper des if-Blocks beginnt: in Zeile 5. Daher wird in das goto in Zeile 3, der einzigen Stelle, bei der zum Körper gesprungen werden soll, die Zeile 5 als Ziel eingetragen. Die anderen beiden goto sollen ans Ende springen. Es ist weiterhin nicht bekannt, wo dieses Ende ist – es könnte ja noch weiterer Code unten hinzukommen. Diese Platzhalter können daher erst später aufgelöst werden, wenn der Pascal-Code um den if-Block herum, außerhalb dieses Beispiels, verarbeitet wurde.

Nehmen wir denselben Pascal-Code wie oben und ersetzen das and durch ein or, erhalten wir folgenden Zwischencode:

        if variable1 < 5 goto 5    
        goto 3
        if variable2 == 10 goto 5    
        goto ...
        variable1 = 10
    

Beim or gilt: Wenn die erste Expression wahr ist, kann bereits zum Körper des if-Blocks nach Zeile 5 gesprungen werden. Ansonsten muss die zweite Expression in Zeile 3 überprüft werden. Daher wird in Zeile 2 nach Zeile 3 gesprungen. Ist die zweite Expression wahr, kann ebenfalls nach Zeile 5 gesprungen werden. Nur falls nach der ersten auch die zweite Expression falsch ist, muss in Zeile 4 an das noch unbekannte Ende des if-Blocks gesprungen werden.

In diesem Beispiel könnte man das goto in Zeile 2 weglassen, da ja automatisch die nächste Zeile ausgeführt würde. Das würde aber den Compiler komplizierter machen, der das goto dort generiert, weil es manchmal (etwa beim and) gebraucht wird. Daher lassen wir das lieber.

Folgende (und weitere) Aktionen können also bei jedem Kompilierschritt getätigt werden:

Schleifen im Zwischencode

        variable1 = 0
        variable2 = 0
        while variable1 < 5 do
        begin
            variable1 = variable1 + 1
            variable2 = variable2 + variable1
        end
    

Die weniger Pascal-Versierten werden sich freuen zu erfahren, dass begin und end einfach gesagt nichts anderes sind als geschwiffene Klammern in vielen anderen Programmiersprachen.

Eine solche normale kopfgesteuerte Schleife könnte so im Zwischencode aussehen:

        variable1 = 0
        variable2 = 0
        if variable1 < 5 goto 5    
        goto ...
        variable1 = variable1 + 1
        variable2 = variable2 + variable1
        goto 3
    

Solange die Bedingung noch wahr ist, wird zur Laufzeit immer wieder in den Körper der Schleife gesprungen (von Zeile 3). Das goto danach (in Zeile 7) darf nicht vergessen werden, um nach einem Schleifendurchlauf wieder an den Beginn zu springen und die Bedingung erneut zu überprüfen. Ist die Bedindung nicht mehr wahr, wird an einen noch unbekannten Ort ans Ende gesprungen.

Ginge man die Erzeugung dieses Zwischencodes wieder Schritt für Schritt durch, würde man zwei Dinge bemerken, die für später relevant sind:

Schleifen funktionieren im Zwischencode wie if-Blöcke. Die Expressions können daher ebenso verwendet werden. Zusätzlich muss am Ende des Schleifenkörpers ein Sprung zurück an den Anfang ausgeführt werden.