Funktionales Programmieren und Compilerbau

Werbung
Technische Universität München
Institut für Informatik
Prof. Tobias Nipkow, Ph.D.
N. Schirmer, M. Blume
WS 2002/2003
Dokumentation
26. November 2002
Funktionales Programmieren und Compilerbau
Dokumentation und Überblick für Teil 2 des Programmirpraktikums
Inhaltsverzeichnis
1 Aufgabenstellung
1
2 Beschreibung der Quellsprache (SimpleJava)
2
2.1
Konkrete Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
2.2
Abstrakte Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
2.3
Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
3 Die PMI-Maschine
3
3.1
Aufbau
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3
3.2
Der Speicher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
3.3
Adressierungsarten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
3.4
Befehlssatz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
3.5
Zu implementierende Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
4 Der Compiler
4.1
1
8
Hilfsmodule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
4.1.1
Der Datentyp Error . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
4.1.2
Parserkombinatoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
4.2
Beispielprogramm
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
4.3
Lexikalische Analyse (Scanner) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
4.4
Syntaktische Analyse (Parser) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
13
4.5
Semantische Analyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
4.6
Codegenerator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
4.7
Test mit der PMI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16
Aufgabenstellung
Ziel des 2. Teils des Praktikums ist es, einen Compiler für die Programmiersprache SimpleJava zu schreiben und
das erzeugte Maschinenprogramm auf einer virtuellen Maschine auszuführen.
In Abschnitt 2 wird die Syntax der Sprache SimpleJava beschrieben, eine selbstdefinierte einfache Form von
Java. Sie verwendet im wesentlichen diesselbe Syntax wie Java, ist aber vom Umfang her sehr beschränkt (z.B.
keine Klassen, einziger Datentyp int).
Als virtuelle Maschine wird die PMI-Maschine gewählt, bekannt aus der Informatik II Vorlesung bei Prof.
Brügge. Abschnitt 3 enthält eine Erläuterung, wie die PMI-Maschine in Haskell umgesetzt ist.
In Abschnitt 4 werden die Schnittstellen der einzelnen Bestandteile des Compilers definiert und anhand eines
Beispiels vorgestellt.
1
Die Implementation erfolgt in vier Teilaufgaben:
• PMI Maschine
• Code Generierung
• Lexer und Parser
• Analyse
Für die Teilaufgaben werden jeweils noch gesonderte Übungsblätter ausgegeben. Bei den Aufgaben sind dann
sowohl die gegebenen Schnittstellen als auch benötigte Hilfsfunktionen zu implementieren.
2
Beschreibung der Quellsprache (SimpleJava)
2.1
Konkrete Syntax
Ein SimpleJava Programm muss den nachfolgenden syntaktischen Regeln entsprechen.
<program>
<separator>
::=
::=
<declaration>∗ <statement>∗
;
<declaration>
::=
int t <variable> [ , <variable> ]∗ <separator>
<statement>
<assignment>
<if-clause>
<while-clause>
<sequence>
::=
::=
::=
::=
::=
<assignment> | <if-clause> | <while-clause> | <sequence>
<variable> = <expression> <separator>
if (<condition>) <statement> [ else <statement> ]?
while (<condition>) <statement>
{ <statement>∗ }
<expression>
<condition>
::=
::=
<variable> | <number> | (<expression> <arithOp> <expression>)
<expression> <relOp> <expression>
<arithOp>
<relOp>
::=
::=
+|−|*|/
== | != | > | >= | < | <=
<variable>
<number>
::=
::=
<letter> [ <letter> | <digit> |
<digit>+ | (− <digit>+ )
<letter>
<digit>
::=
::=
a | b | ... | z | A | ... | Z
0 | 1 | ... | 9
2.2
]∗
Abstrakte Syntax
Um das SimpleJava Programm compilieren zu können, muss die konkrete Syntax in eine abstrakte Form gebracht
werden. Die abstrakte Syntax ist in Haskell so festgelegt (siehe auch die Dateien SJAbstractSyntax.hs und
SJOperators.hs):
data Program = Program Declarations [Statement]
deriving (Show, Read)
type Declarations = [Identifier]
data Statement = Assignment Identifier Expression
| If Condition Statement Statement
2
| While Condition Statement
| Sequence [Statement]
deriving (Show, Read)
data Expression = Variable Identifier
| Number Integer
| Compound Expression ArithOp Expression
deriving (Show, Read)
data Condition = Condition Expression RelOp Expression
deriving (Show, Read)
type Identifier = String
data ArithOp =
Plus
| Minus
| Times
| Divide
deriving (Eq, Show, Read)
data RelOp =
Equal
| NotEqual
| LessThan
| LessOrEqual
| GreaterThan
| GreaterOrEqual
deriving (Eq, Show, Read)
2.3
Beispiel
Der Zusammenhang zwischen abstrakter und konkreter Syntax wird nun noch anhand eines Beispiels verdeutlicht. Eine mögliche Codezeile in einem SimpleJava Programm ist:
if (a < 5) a = (a + 1); else a = 0;
Die abstrakte Syntax sieht dann so aus:
If (Condition (Variable "a") LessThan (Number 5))
(Assignment "a" (Compound (Variable "a") Plus (Number 1)))
(Assignment "a" (Number 0))
3
3.1
Die PMI-Maschine
Aufbau
Die PMI-Maschine besteht aus drei Registern (program counter, stack pointer, heap pointer), einem Statuswort
und einem linearen Speicher. Zusätzlich können Größe und Lage von Stack und Heap im Speicher festgelegt
werden.
Das Statuswort enthält neben den bekannten Flags halted, negative und zero ein weiteres Flag err, das die
Fehlermeldung enthält, wenn die Maschine aufgrund eines Fehlers angehalten wurde.
Die Datentyp-Definitionen sehen folgendermaßen aus:
data Machine = Machine {register :: Register,
status :: Status,
store :: Store,
properties :: StoreProperties}
deriving (Eq, Show, Read)
3
data Register = Register {pc :: StoreAddress,
sp :: StoreAddress,
hp :: StoreAddress}
deriving (Eq, Show, Read)
data Status = Status {halted :: Bool,
negative :: Bool,
zero :: Bool,
err :: Error ()}
deriving (Eq, Show, Read)
Für den Datentyp Error siehe 4.1.
3.2
Der Speicher
Der Speicher unserer Maschine weist einige Besonderheiten auf. Zunächst muss eine Speicherzelle nicht unbedingt
existieren. Da die Maschine virtuell ist, sollen der Übersicht halber nicht alle Speicherzellen aufgeführt werden,
sondern nur immer die gerade benutzten. Der Speicher wird mittels des abstrakten Datentyps Table realisiert
und nur wenn eine Speicherzelle explizit in der Tabelle aufgeführt ist, ist sie auch vorhanden. Steht gerade kein
definierter Wert in einer Zelle, so liefert ein Lesezugriff die ¨Zufallszahl¨ (-1234) zurück, da man in einer reellen
Maschine ja auch nie wissen kann, was gerade im Speicher steht.
In einer Speicherzelle kann wie gewohnt entweder eine Zahl oder ein Befehl stehen. Da Befehle aber nicht
verschlüsselt werden, wird der Datentyp StoreValue eingeführt, der die Konstruktoren Value für Zahlen und
Code für Maschineninstruktionen enthält. Dadurch kann es bei Leseaktionen im Speicher zu folgenden Problemen
kommen:
• Es wird ein Befehl erwartet, aber eine Zahl gefunden. Der PC zeigt also auf eine Speicherzelle, in der kein
Befehl steht. In diesem Fall wird die Maschine mit einer Fehlermeldung angehalten.
• Es wird eine Zahl erwartet, aber ein Befehl gefunden. In diesem Fall wird die Zahl (-1234) zurückgegeben.
type Store = Table StoreAddress StoreValue
type StoreAddress = Integer
data StoreValue =
Value Integer
| Code MachineInstruction
deriving (Eq, Show, Read)
Der Datentyp MachineInstruction wird in 3.4 erläutert und der Datentyp Table ist in der Aufgabe vorgegeben.
Mit den StoreProperties kann man Lage und Größe von Heap und Stack festlegen. Standardmäßig sind die
Werte folgendermaßen initialisiert:
heapStart = 8000
heapSize = 4000
stackStart = 16000
stackSize = 4000
Der Speicherbereich bis 8000 ist somit für den Code reserviert.
data StoreProperties = StoreProperties {heapStart :: StoreAddress,
heapSize :: Integer,
stackStart :: StoreAddress,
stackSize :: Integer}
deriving (Eq, Show, Read)
3.3
Adressierungsarten
Es werden die gleichen Adressierungsarten wie in der PMI unterstützt. Dafür werden folgende Datentypen
definiert:
4
data MachineAddress =
Immediate Address
| Direct Address
| Indirect Address
deriving (Eq, Show, Read)
data Address =
Absolute StoreAddress
| PC Offset
| HP Offset
| SP Offset
deriving (Eq, Show, Read)
type Offset = Integer
Die folgende Tabelle zeigt, wie die Befehle des PMI-Assemblers in Haskell aussehen.
unmittelbare Adressierung
direkte Adressierung
indirekte Adressierung
3.4
PMI-Assemblersprache
push 1
push pc
push hp+4
push sp-2
push @1
push @pc
push @hp+4
push @sp-2
push >1
push >pc
push >hp+4
push >sp-2
Haskell
Push (Immediate (Absolute 1))
Push (Immediate (PC 0))
Push (Immediate (HP 4))
Push (Immediate (SP (-2)))
Push (Direct (Absolute 1))
Push (Direct (PC 0))
Push (Direct (HP 4))
Push (Direct (SP (-2)))
Push (Indirect (Absolute 1))
Push (Indirect (PC 0))
Push (Indirect (HP 4))
Push (Indirect (SP (-2)))
Befehlssatz
In Haskell sind die PMI Befehle durch den Datentyp MachineInstruction festgelegt.
type MachineProgram = [MachineInstruction]
data MachineInstruction =
Halt
| Push MachineAddress
| Pop MachineAddress
| Del
| Add
| Sub
| Mult
| Div
| Comp
| Jump MachineAddress
| JmpN MachineAddress
| JmpZ MachineAddress
deriving (Eq, Show, Read)
Die PMI-Befehle jsr und ret werden nicht implementiert, da Unterprogramme nicht zum Sprachumfang von
SimpleJava gehören.
Die Bedeutung der Befehle sind im einzelnen (dabei bedeutet @pre, dass die Werte vor der Ausführung des
Befehls gemeint sind, also die Precondition-Werte):
Halt
Hält die Maschine an und beendet dadurch die Ausführung des Programms.
5
Postconditions:
Status.halted = true
pc = pc@pre
Push value
Legt den angegebenen Wert als oberstes Element auf dem Stapel ab.
Preconditions:
sp > StoreProperties.stackStart - StoreProperties.stackSize
-- Stapel nicht voll
Postconditions:
sp = sp@pre - 1
pc = pc@pre + 1
Store.getValue(sp) = value
Pop address
Holt das oberste Element vom Stapel und legt es bei der angegebenen Adresse ab.
Preconditions:
sp < StoreProperties.stackStart
-- Stapel ist nicht leer
Postconditions:
sp = sp@pre + 1
pc = pc@pre + 1
Store.getValue(address) = [email protected](sp@pre)
Del
Entfernt das oberste Element vom Stapel.
Preconditions:
sp < StoreProperties.stackStart
-- Stapel ist nicht leer
Postconditions:
sp = sp@pre + 1
pc = pc@pre + 1
Add
Holt die obersten zwei Elemente vom Stapel, addiert sie, und legt das Ergebnis als
oberstes Element auf dem Stapel ab. Warnung: Ein eventuell stattfindender Überlauf wird
nicht registriert!
Preconditions:
sp + 2 <= StoreProperties.stackStart
-- Stapel enthaelt zwei Elemente
Postconditions:
sp = sp@pre + 1
pc = pc@pre + 1
Store.getValue(sp)
= [email protected](sp@pre + 1) + [email protected](sp@pre)
6
Sub
Holt die obersten zwei Elemente vom Stapel, subtrahiert sie, und legt das Ergebnis als
oberstes Element auf dem Stapel ab. Warnung: Ein eventuell stattfindender Überlauf wird
nicht registriert!
Preconditions:
sp + 2 <= StoreProperties.stackStart
-- Stapel enthaelt zwei Elemente
Postconditions:
sp = sp@pre + 1
pc = pc@pre + 1
Store.getValue(sp)
= [email protected](sp@pre + 1) - [email protected](sp@pre)
Mult
Holt die obersten zwei Elemente vom Stapel, multipliziert sie, und legt das Ergebnis als
oberstes Element auf dem Stapel ab. Warnung: Ein eventuell stattfindender Überlauf
wird nicht registriert!
Preconditions:
sp + 2 <= StoreProperties.stackStart
-- Stapel enthaelt zwei Elemente
Postconditions:
sp = sp@pre + 1
pc = pc@pre + 1
Store.getValue(sp)
= [email protected](sp@pre + 1) * [email protected](sp@pre)
Div
Holt die obersten zwei Elemente vom Stapel, dividiert sie (ganzzahlige Division), und
legt das Ergebnis als oberstes Element auf dem Stapel ab. Warnung: Ein eventuell
stattfindender Überlauf wird nicht registriert!
Preconditions:
sp + 2 <= StoreProperties.stackStart
-- Stapel enthaelt zwei Elemente
Postconditions:
sp = sp@pre + 1
pc = pc@pre + 1
Store.getValue(sp)
= [email protected](sp@pre + 1) / [email protected](sp@pre)
Comp
Vergleicht das oberste Element vom Stapel mit 0.
Preconditions:
sp < StoreProperties.stackStart
-- Stapel ist nicht leer
Postconditions:
pc = pc@pre + 1
Status.zero = (Store.getValue(sp) == 0)
7
Status.negative = (Store.getValue(sp) < 0)
Jump address
Springt zu der angegebenen Adresse
Postconditions:
pc = address
JmpN address
Springt zu der angegebenen Adresse, wenn der letzte Vergleich mit compare() einen
negativen Wert geliefert hat.
Postconditions:
Status.negative == true implies pc = address
Status.negative == false implies pc = pc@pre + 1
JmpZ address
Springt zu der angegebenen Adresse, wenn der letzte Vergleich mit compare() den
Wert 0 geliefert hat.
Postconditions:
Status.zero == true implies pc = address
Status.zero == false implies pc = pc + 1
3.5
Zu implementierende Funktionen
Es wird eine Funktion run benötigt, die das gesamte Programm ausführt, das in der Maschine geladen ist.
Die Funktion step führt nur einen Einzelschritt aus. Die Maschine wird durch ausführen des nächsten Befehls
(adressiert durch pc) in den Folgezustand überführt. Die Schnittstellen sind wie folgt definiert:
run :: Machine -> Machine
step :: Machine -> Machine
4
4.1
4.1.1
Der Compiler
Hilfsmodule
Der Datentyp Error
Der Datentyp Error wird zur Fehlerbehandlung benötigt (s. Datei Error.hs).
data Error a = Ok a | Fail String
deriving (Eq, Show, Read)
Gelingt eine Aktion wird Ok <Ergebnis> zurückgegeben, ansonsten Fail <Fehlermeldung>.
Will man bei zwei Ergebnissen überprüfen, ob beide Aktionen ohne Fehler ausgeführt wurden, verwendet man
den Infix-Operator &.
infixr 5
(&):: Error a -> Error a
(&) (Fail str) _
(&) _
(Fail str)
(&) _
_
-> Error ()
= Fail str
= Fail str
= Ok ()
Will man nun auf ein Ergebnis vom Typ Error eine weitere Funktion f anwenden, gibt man die Fehlermeldung
x aus, falls das Ergebnis Fail x ist. Ansonsten, falls das weiter zu verarbeitende Ergebnis Ok a ist, führt man
die Funktion f mit Eingabeparameter a aus. Hierfür gibt es den Infix-Operator >=>.
8
infixr 3 >=>
(>=>):: Error a -> (a -> Error b) -> Error b
(>=>) (Fail str) _ = Fail str
(>=>) (Ok
a ) f = f a
Außerdem wird die Funktion isOk bereitgestellt, die prüft, ob die Eingabe vom Typ Error von der Form Ok a
ist.
isOk :: Error a -> Bool
isOk (Ok x) = True
isOk _
= False
Wenn man schon weiß, dass kein Fehler aufgetreten ist und man möchte nur das eigentliche Ergebnis haben,
ohne den Konstruktor Ok des Error-Datentyps, so verwendet man die Funktion theOk.
theOk :: Error a -> a
theOk (Ok x) = x
4.1.2
Parserkombinatoren
Für die Umsetzung von Scanner (Lexikalische Analyse s. 4.3) und Parser (Syntaktische Analyse s. 4.4) gibt
es sehr nützliche Hilfsfunktionen (s.Datei Parsecombs.hs). Die zugrundeliegende Datenstruktur Parse ist wie
folgt deklariert:
type Parse a b = [a] -> Error (b, [a])
Ein Parser ist also eine Funktion, die als Eingabewert eine Liste vom Typ a erwartet. Sind die am Anfang
stehenden Werte in der Liste genau die Werte, die der Parser erwartet, so spaltet der Parser die Werte von der
Liste ab, wandelt sie in einen anderern Typ um und gibt ein Tupel zusammen mit dem Konstruktor Ok zurück.
Das Tupel enthält als erstes Argument den umgewandelten Typ und als zweites Argument die restlichen Werte
der Liste. Ist der Anfang der Eingabeliste jedoch nicht das, was der Parser erwartet, so wird Fail ¨Syntax
error¨ zurückgegeben.
Beispiel: Ein ganz einfacher Parser ist der alphaParser, der überprüft ob das nächste Zeichen in der Liste
aus dem Alphabet ist (die Hilfsfunktion spot ist in der Datei Parsecombs.hs definiert):
alphaParser :: Parse Char Char
alphaParser = spot isAlpha
Übergibt man ihm beispielsweise den String ¨Haskell¨ erhält man:
alphaParser "Haskell" ==> Ok (’H’, "askell")
Enthält der String aber am Anfang eine Zahl, ist die Ausgabe:
alphaParser "123 Haskell" ==> Fail "Syntax error"
Beispiel: In vorherigen Beispiel war der Typ des Parsers Parse Char Char. Laut Definition müssen aber
der Typ der Eingabeliste und der abgespaltenen Werte nicht identisch sein. Deshalb wird jetzt jedes erkannte
Zeichen in ihren ASCII-Wert umgwandelt (die Hilfsfunktion >>> ist in der Datei Parsecombs.hs definiert).
alpha2ASCIIParser :: Parse Char Int
alpha2ASCIIParser = spot isAlpha >>> ord
Übergibt man dem Parser jetzt den String ¨Haskell¨ erhält man:
alpha2ASCIIParser "Haskell" ==> Ok (72, "askell")
Einen Parser für eine komplizierte Sprache setzt man am besten nach dem ¨divide and conquer¨ Verfahren
aus vielen kleinen Parsern zusammen. Damit man aber alle kleinen Parser in einer Funktion verwenden kann,
werden noch Hilfsfunktionen benötigt, die diese Parser miteinander kombinieren (Parserkombinatoren), um so
den gewünschten Parser zu erhalten.
9
Beispiel: Dieses Beispiel soll zeigen wie die Kombination von Parsern funktioniert. Dabei ist es unwichtig,
wie die einzelnen Parser implementiert sind. Für SimpleJava sieht der while-Schleifen Parser folgendermaßen
aus (für die Definition von Symbol siehe 4.3 und für Statement siehe 2.2):
whileParser :: Parse Symbol Statement
whileParser = token WhileSym $- conditionParser &&& statementParser
>>> (\(cond, stmt) -> While cond stmt)
In diesem Beispiel sieht man, dass der Parser für while-Schleifen durch die Kombination von anderen Parsern
entsteht. Diese sind im einzelnen:
• token WhileSym
• conditionParser
• statementParser
Diese Parser können wiederum durch Kombination von Parsern implementiert sein. Außerdem werden in diesem
Beispiel folgenden Hilfsfunktionen zur Kombination verwendet (für die Bedeutung siehe nächstes Beispiel und
Beschreibung der Hilfsfunktionen der Datei Parsecombs.hs):
• $• &&&
• >>>
An diesem Beispiel sieht man, dass die Hilfsfunktionen selbst schon Parser sind, denn nur so ist gewährleistet,
dass man am Ende wieder einen Parser erhält.
Beispiel: Die Arbeitsweise des while-Parsers wird nun noch anhand eines konkreten Beispiels besser verdeutlicht. Eine mögliche while-Schleife in einem SimpleJava Program ist:
while (x < y) x = (x+1);
Nach der lexikalischen Analyse erhält man als Eingabeliste für den while-Parser:
[WhileSym,OpenPar,Ident "x",RelOp LessThan,Ident "y",ClosePar,
Ident "x",AssignOp,OpenPar,Ident "x",ArithOp Plus,Num 1,ClosePar,Semicolon]
Durch den Aufruf ¨whileParser <Eingabeliste>¨ erhält man:
Ok (While (Condition (Variable "x") LessThan (Variable "y"))
(Assignment "x" (Compound (Variable "x") Plus (Number 1))),[])
Die einzelnen Parser haben dabei folgende Ein- und Ausgaben:
Parser
token WhileSym
$-
conditionParser
&&&
statementParser
>>> (\(co...mt)
Eingabe
Ausgabe
Erläuterung
[WhileSym,OpenPar,
Ok (WhileSym,[OpenPar,
erkennt Symbol
Ident ¨x¨, ...,Semicolon] Ident ¨x¨, ...,Semicolon])
WhileSym
Wirft das erkannte Symbol WhileSym weg und ruft conditionParser auf. Das Symbol
wird verworfen, da es für die abstrakte Syntax keine Bedeutung hat, aber im SimpleJava
Code die while-Schleife einleitet.
[OpenPar,Ident ¨x¨, ...,
Ok (Condition (Variable ¨x¨) erkennt die
ClosePar,Semicolon]
LessThan (Variable ¨y¨),
Bedingung der
[Ident ¨x¨,AssignOp, ...,
while-Schleife
ClosePar,Semicolon])
Merkt sich die Bedingung, ruft den statementParser auf und kombiniert am Ende die
Ergebnisse von conditionParser und statementParser zu einem Tupel
[Ident ¨x¨,AssignOp, ..., Ok (Assignment ¨x¨ (Compound erkennt den
ClosePar,Semicolon]
(Variable ¨x¨) Plus
Anweisungsteil
(Number 1)),[])
der while-Schl.
Wandelt das Ergebnis aus allen obigen Parsern in ein Statement um.
10
In der Datei Parsecombs.hs sind folgende Hilfsfunktionen implementiert
:
succeed ist der Parser, der grundsätzlich ein Zeichen akzeptiert, unabhängig von der Eingabeliste.
succeed :: b -> Parse a b
succeed val xs = Ok (val, xs)
Ähnlich verhält sich empty. Dieser Parser akzeptiert immer die leere Liste und gibt als Restliste die Eingabeliste
zurück.
empty :: Parse a [b]
empty xs = Ok ([], xs)
Mit dem Infix-Operator ||| hat man alternativ zwei Parser zur Verfügung. Akzeptiert der erste Parser kein
Eingabezeichen aus der Liste, wird der zweite ausprobiert. Nur wenn keiner der beiden Parser passt, erhält man
eine Fehlermeldung.
infixl 0 |||
(|||) :: Parse a b -> Parse a b -> Parse a b
(|||) p1 p2 xs = eval (p1 xs) p2 xs
where
eval :: Error (b, [a]) -> Parse a b -> Parse a b
eval (Fail _) p2 xs = p2 xs
eval res _ _ = res
Zwei Parser lassen sich hintereinander ausführen mit dem Infix-Operator &&&. Das Ergebnis beider Parser wird
als Tupel vom Typ (b,c) zurückgegeben.
infixr 5 &&&
(&&&) :: Parse a b -> Parse a c -> Parse a (b,c)
(&&&) p1 p2 xs = p1 xs >=> (eval p2)
where
eval p2 (x, ys) = p2 ys >=> (combine x)
combine x (y, zs) = Ok ((x, y), zs)
Hat ein Parser ein Eingabezeichen (oder mehrere) akzeptiert, so kann man auf dieses Ergebnis mit dem InfixOperator >>> eine Funktion f anwenden. Die Restliste, die an den nächsten Parser weitergegeben wird, bleibt
unverändert.
infix 3 >>>
(>>>) :: Parse a b -> (b -> c) -> Parse a c
(>>>) p f xs = p xs >=> \(x, ys) -> Ok (f x, ys)
Die Funktion many führt einen Parser so oft hintereinander aus, bis der Parser kein Eingabezeichen mehr erkennt.
Die akzeptierten Werte werden in einer Liste zusammengefasst.
many :: Parse a b -> Parse a [b]
many p = (p &&& many p) >>> (uncurry (:))
||| (succeed [])
many1 hat die gleiche Funktionalität wie many, außer dass mindestens ein Zeichen der Eingabeliste akzeptiert
werden muss.
many1 :: Parse a b -> Parse a [b]
many1 p = p &&& many p >>> (uncurry (:))
Der Parser spot akzeptiert ein Eingabezeichen genau dann, wenn es eine bestimmte Bedingung (boolesche
Funktion) erfüllt.
11
spot :: (a -> Bool) -> Parse a a
spot f (x:xs)
| f x
= Ok (x, xs)
| otherwise = Fail "Syntax error"
spot f []
= Fail "Syntax error"
Ein Sonderfall von spot ist die Funktion token. Dieser Parser erwartet ein ganz bestimmtes Eingabezeichen.
token :: Eq a => a -> Parse a a
token t = spot (==t)
Will man nicht nur ein einzelnes Zeichen vorgeben, sondern eine Liste von Zeichen, so gibt es dafür die Funktion
tokenList.
tokenList :: Eq a => [a] -> Parse a [a]
tokenList []
= empty
tokenList (x:xs) = token x &&& tokenList xs >>> (uncurry (:))
Der Infix-Operator $- bewirkt, dass zwei Parser hintereinander angewandt werden. Allerdings wird das akzeptierte Ergebnis des ersten Parsers weggeworfen und nur das des zweiten ausgegeben.
infix 6 $($-) :: Parse a b -> Parse a c -> Parse a c
($-) p1 p2 = p1 &&& p2 >>> snd
Ebenso verhält sich -$, nur dass das Ergebnis des zweiten Parsers verworfen und das des ersten zurückgegeben
wird.
infix 6 -$
(-$) :: Parse a b -> Parse a c -> Parse a b
(-$) p1 p2 = p1 &&& p2 >>> fst
Schließlich gibt es noch die Funktion optional, die zunächst versucht einen Parser auf die Eingabeliste anzuwenden. Gelingt dies nicht, wird einfach ein vorgegebenes Ergebnis akzeptiert.
optional :: Parse a b -> b -> Parse a b
optional p def = p
||| (succeed def)
4.2
Beispielprogramm
Dieses SimpleJava Programm berechnet die Fakultätsfunktion. x ist der Eingabewert, das Ergebnis wird in der
Variablen erg gespeichert.
int x, erg;
x = 5;
if (x <
erg =
else
{
erg =
while
{
erg
x =
}
}
0)
(-1);
1;
(x > 1)
= (erg * x);
(x - 1);
12
Dieses Beispiel wird im folgenden bei der Erklärung der einzelnen Teile des Compilers verwendet.
4.3
Lexikalische Analyse (Scanner)
Der Scanner liest das Quellprogramm in Form einer Zeichenfolge und zerlegt diese Zeichenfolge in eine Folge
von lexikalischen Einheiten der Programmiersprache, Symbole genannt. Leerzeichen und Sonderzeichen, wie
Zeilenschaltung und Tabulatoren werden ausgesiebt. Für die Symbole ist der Datentyp Symbol folgendermaßen
definiert (siehe auch Datei SJSymbol):
data Symbol =
ArithOp ArithOp
| RelOp RelOp
|
|
|
|
AssignOp
IfSym
ElseSym
WhileSym
| IntType
| Ident Identifier
| Num Integer
-- + - * /
-- == != < <= > >=
-----
=
if
else
while
-- int
-- Variablenname
-- Zahl
| OpenPar
-| ClosePar
-| OpenBrace
-| CloseBrace
-| Colon
-| Semicolon
-deriving (Eq, Show, Read)
(
)
{
}
,
;
Der Scanner wird hier durch die Funktion scan realisiert. Falls ein Eingabezeichen nicht erkannt wird, gibt die
Funktion einen Fehler mit Hilfe des Error Datentyps zurück.
scan :: [Char] -> Error [Symbol]
Beispiel:
Bei Eingabe des Beispielprogramms (s. 4.2) erhält man folgende Ausgabe:
Ok [IntType,Ident "x",Colon,Ident "erg",Semicolon,
Ident "x",AssignOp,Num 5,Semicolon,
IfSym,OpenPar,Ident "x",RelOp LessThan,Num 0,ClosePar,
Ident "erg",AssignOp,Num (-1),Semicolon,
ElseSym,
OpenBrace,
Ident "erg",AssignOp,Num 1,Semicolon,
WhileSym,OpenPar,Ident "x",RelOp GreaterThan,Num 1,ClosePar,
OpenBrace,
Ident "erg",AssignOp,OpenPar,Ident "erg",ArithOp Times,Ident "x",ClosePar,Semicolon,
Ident "x",AssignOp,OpenPar,Ident "x",ArithOp Minus,Num 1,ClosePar,Semicolon,
CloseBrace,
CloseBrace]
4.4
Syntaktische Analyse (Parser)
Die syntaktische Analyse soll die über die lexikalische hinausgehende Struktur des Programms herausfinden. Sie
kennt den Aufbau von Ausdrücken, Anweisungen, Deklarationen und von Auflistungen von solchen Konstrukten,
und sie versucht, in der gegebenen Symbolfolge die Struktur eines Programms zu erkennen.
Unser Parser erzeugt aus einer Symbolliste, die der Scanner liefert, einen abstrakten Syntaxbaum. Wie dieser
Baum aufgebaut wird, ist im Modul SJAbstractSyntax definiert. Im Fehlerfall wird wieder eine Fehlermeldung
zurückgegeben.
13
parser :: [Symbol] -> Error Program
Beispiel: Die Ausgabe des Scanners wird im Parser weiterverarbeitet. Das Ergebnis bei unserem Beispielprogramm sieht dann so aus:
Ok (Program
["x","erg"]
[Assignment "x" (Number 5),
If (Condition (Variable "x") LessThan (Number 0))
(Assignment "erg" (Number (-1)))
(Sequence
[Assignment "erg" (Number 1),
While (Condition (Variable "x") GreaterThan (Number 1))
(Sequence
[Assignment "erg" (Compound (Variable "erg") Times (Variable "x")),
Assignment "x" (Compound (Variable "x") Minus (Number 1))]
)]
)
]
)
4.5
Semantische Analyse
Bei der semantischen Analyse wird überprüft, ob
• alle im Programm verwendeten Variablen deklariert wurden;
• keine Variablen mehrfach deklariert wurden;
• die Variablen initialisiert wurden bevor sie im Programm verwendet werden;
• alle Zuweisungen typkonform sind und ob alle Funktionsparameter den erwarteten Typ haben. Daher
nennt man den Analyser auch Typechecker. SimpleJava enthält jedoch nur den Datentyp int. Deshalb ist
eine Prüfung der Typen in diesem Fall nicht nötig.
Der Analyser für SimpleJava überprüft einen abstrakten Syntaxbaum auf die drei ersten Punkte hin und liefert
eine Fehlermeldung, wenn einer davon nicht erfüllt ist. Ansonsten ist die Ausgabe einfach Ok ().
analyse :: Program -> Error ()
Beispiel:
In unserem Beispiel liefert der Analyser das Ergebnis
Ok ()
Dies war zu erwarten, da das Eingabeprogramm korrekt ist.
4.6
Codegenerator
Der Codegenerator übersetzt nach der semantischen Analyse den Syntaxbaum, den der Parser geliefert hat, in
ein Maschinenprogramm, das von unserer virtuellen PMI Maschine ausgeführt werden kann.
generateCode :: Program -> MachineProgram
Variablen werden als ¨lokale Variablen¨ übersetzt, sodass man später vielleicht noch Prozeduren in SimpleJava
einbauen kann. Jedes SimpleJava Programm wird also als Prozedur betrachtet. Für jeden Prozeduraufruf wird
zur Laufzeit ein Bereich auf dem Stack angelegt, in dem die Parameter der Prozedur (in diesem Fall nicht vorhanden), die lokalen Variablen und der Operandenstack liegen. Dieser Bereich wird Stackframe genannt. Lokale
Variablen liegen am Anfang des Stackframes. Um die Variablen später zu adressieren, braucht man immer den
14
Anfang des Stackframes. Die PMI hat aber kein Register für diese Adresse, sondern nur ein Register, das auf
das oberste Element des Stacks zeigt. Man kann aber zur Übersetzungszeit den Anfang des Stackframes berechnen. Dazu muss zunächst eine Symboltabelle aufgebaut werden, die jeder deklarierten Variablen einen Index
zuweist. Die Variablen werden dann in der Reihenfolge aufsteigender Indizes auf dem Stack abgelegt. Außerdem
muss bei der Übersetzung immer ein Offset mitgeführt werden, der angibt, wieviele Elemente (einschließlich der
lokalen Variablen) gerade auf dem Stack liegen. Mit Hilfe des Offsets und der Indexnummer der Variablen in
der Symboltabelle kann zu jeder Zeit der Generierung einer neuer Offset berechnet werden, der den Abstand
der Variablen zum Stack Pointer angibt.
Beispiel: Nachdem der Syntaxbaum aus 4.4 semantisch analysiert und als korrekt bewertet wurde, kann nun
der Maschinencode generiert werden. Das Ergebnis sieht wie folgt aus:
[Push (Immediate (Absolute 0)),
Push (Immediate (Absolute 0)),
Push (Immediate (Absolute 5)),
Pop (Immediate (SP 2)),
Push (Direct (SP 1)),
Push (Immediate (Absolute 0)),
Sub,
Comp,
Del,
JmpN (Immediate (PC 3)),
Push (Immediate (Absolute 0)),
Jump (Immediate (PC 2)),
Push (Immediate (Absolute 1)),
Comp,
Del,
JmpZ (Immediate (PC 4)),
Push (Immediate (Absolute (-1))),
Pop (Immediate (SP 1)),
Jump (Immediate (PC 26)),
Push (Immediate (Absolute 1)),
Pop (Immediate (SP 1)),
Jump (Immediate (PC 9)),
Push (Direct (SP 0)),
Push (Direct (SP 2)),
Mult,
Pop (Immediate (SP 1)),
Push (Direct (SP 1)),
Push (Immediate (Absolute 1)),
Sub,
Pop (Immediate (SP 2)),
Push (Direct (SP 1)),
Push (Immediate (Absolute 1)),
Sub,
Comp,
Del,
JmpN (Immediate (PC 4)),
JmpZ (Immediate (PC 3)),
Push (Immediate (Absolute 1)),
Jump (Immediate (PC 2)),
Push (Immediate (Absolute 0)),
Comp,
Del,
JmpZ (Immediate (PC 2)),
Jump (Immediate (PC (-21))),
Halt]
15
4.7
Test mit der PMI
Das Maschinenprogramm, das mit dem Codegenerator erzeugt wird, kann nun mit Hilfe der PMI getestet
werden. Bei Eingabe des Beispielprogramms, stehen im Speicher am Ende folgende Werte:
Stack nach Ablauf des Programms:
(15999,Value 1)
-- x = 1
(15998,Value 120) -- erg = 120
16
Herunterladen